npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

next-optimistic-router

v0.0.4

Published

> This library makes navigation truly instantaneous, without prefetching or caching. Also you can think about that library as a glue between next.js and tanstack-query/swr.

Downloads

77

Readme

next-optimistic-router

This library makes navigation truly instantaneous, without prefetching or caching. Also you can think about that library as a glue between next.js and tanstack-query/swr.

Installation

$ npm install next-optimistic-router
$ yarn add next-optimistic-router

Usage with @tanstack/react-query

1) Wrap _app into OptimisticRouterProvider

import "@/styles/globals.css";
import type { AppProps } from "next/app";
import singletonRouter from 'next/dist/client/router';
import { handleOptimisticNavigation, OptimisticRouterProvider } from 'next-optimistic-router';
import { DehydratedState, HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React, { useLayoutEffect } from 'react';

export default function App({ Component, pageProps }: AppProps<{ dehydratedState: DehydratedState}>) {
  const router = useRouter();

  const [queryClient] = React.useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1 * 60 * 1000,
        gcTime: 5 * 60 * 1000,
      }
    }
  }))

  useLayoutEffect(() => {
    // disable router prefetch completely
    router.prefetch = async () => Promise.resolve(undefined);

    router.beforePopState((state) => {
      handleOptimisticNavigation({
        href: state.as,
        singletonRouter,
        withTrailingSlash: Boolean(process.env.__NEXT_TRAILING_SLASH),
      });
      return true;
    });
  }, [router])

  return (
    <OptimisticRouterProvider singletonRouter={singletonRouter}>
      <QueryClientProvider client={queryClient}>
        <HydrationBoundary state={pageProps.dehydratedState} options={{
          defaultOptions: {},
        }}>
          <Component {...pageProps} />
        </HydrationBoundary>
      </QueryClientProvider>
    </OptimisticRouterProvider>
  );
};

2) Wrap getServerSideProps/getStaticProps functions

Create a file withSSRTanStackQuery.ts

import type { ParsedUrlQuery } from 'querystring';
import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import { dehydrate, QueryClient } from '@tanstack/react-query';

function removeTrailingSlash(route: string) {
  return route.replace(/\/$/, '') || '/'
}

const normalizeResolvedUrl = (resolvedUrl: string) => {
  const pathnameAndQuery = resolvedUrl.split('?') as [string, string];
  let pathname = removeTrailingSlash(pathnameAndQuery[0]);
  let query = pathnameAndQuery[1];


  if (process.env.__NEXT_TRAILING_SLASH && pathname !== '/') {
    pathname = `${pathname}/`
  }
  if (query) {
    const params = new URLSearchParams(query);
    params.forEach((value, key) => {
      if (key.startsWith('nxtP')) {
        params.delete(key);
      }
    });
    query = params.toString();
    if (query)
      return `${pathname}?${query}`
  }
  return pathname;
}

export const withSSRTanStackQuery = <T extends object, Q extends ParsedUrlQuery = ParsedUrlQuery>(getServerSideProps: GetServerSideProps<T, Q>) => async (
  props: GetServerSidePropsContext<Q>,
) => {
  const queryKey = normalizeResolvedUrl(props.resolvedUrl);
  let result: GetServerSidePropsResult<T> | undefined = undefined;
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: [queryKey],
    queryFn: async () => {
      result = await getServerSideProps(props) as { props: T | Promise<T> };
      return result?.props;
    }
  })

  if (!result) {
    return;
  }

  if (!(result as { props: T | Promise<T> }).props) {
    return result;
  }

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

Wrap getServersideProps functions like this

export const getServerSideProps = withSSRTanStackQuery<ArticleItemApi, { slug: string }>(async ({ params }) => {
  const { slug } = params ?? {};
  try {
    const article = await fetchArticle(slug);
    return {
      props: article,
    }
  } catch (e) {
    return {
      notFound: true,
    }
  }
})

Create a file withSSGTanStackQuery.ts

import { ParsedUrlQuery } from 'querystring';
import {
  GetServerSidePropsResult,
  GetStaticProps,
  GetStaticPropsContext
} from 'next';
import { dehydrate, QueryClient } from '@tanstack/react-query';

function removeTrailingSlash(route: string) {
  return route.replace(/\/$/, '') || '/'
}

const normalizePathname = (resolvedUrl: string) => {
  const normalizedResolvedUrl = removeTrailingSlash(resolvedUrl);
  let pathname = normalizedResolvedUrl.split('?')[0];
  if (process.env.__NEXT_TRAILING_SLASH) {
    pathname = `${pathname}/`
  }
  return pathname;
}

export const withSSGTanStackQuery = <T extends object, Q extends ParsedUrlQuery = ParsedUrlQuery>(getPath: (context: Q) => string, getStaticProps: GetStaticProps<T, Q>) => async (
  props: GetStaticPropsContext<Q>,
) => {
  const queryKey = normalizePathname(getPath(props.params!));
  let result: GetServerSidePropsResult<T> | undefined = undefined;
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: [queryKey],
    queryFn: async () => {
      result = await getStaticProps(props) as { props: T | Promise<T> };
      return result?.props;
    }
  })

  if (!result) {
    return;
  }

  if (!(result as { props: T | Promise<T> }).props) {
    return result;
  }

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

And wrap getStaticProps functions like this

export const getStaticProps = withSSGTanStackQuery<ArticleItemApi, { slug: string }>(({ slug }) => `/blog/${slug}/`, async ({ params }) => {
  const { slug } = params ?? {};
  try {
    const article = await fetchArticle(slug)
    return {
      props: article,
    }
  } catch (e) {
    return {
      notFound: true,
    }
  }
})

You must pass getPath function. That callback gets params as an argument and must return path of the page. In example above we return /blog/${slug}/

pages/
└── blog/
    └── [slug]/
        └── index.tsx

3) Create usePageData hook

import { DehydratedState, useQuery, useQueryClient } from '@tanstack/react-query';
import { usePageDataOptions } from 'next-optimistic-router';
import { useRouter } from 'next/router';

export const usePageData = <T>() => {
  const queryClient = useQueryClient();
  const router = useRouter();
  const { queryKey, queryFn } = usePageDataOptions(router, Boolean(process.env.__NEXT_TRAILING_SLASH));
  const placeholderData = typeof window === 'undefined' ? undefined : window.placeholderData;

  const res =  useQuery<unknown, unknown, T>({
    queryKey,
    queryFn: async () => {
      const serverData = queryClient.getQueryData([...queryKey]);
      if (serverData && !res.isStale) {
        return serverData;
      }
      return queryFn().then((props) => {
        const res = props as { dehydratedState: DehydratedState};
        return res?.dehydratedState ? res.dehydratedState.queries[0].state.data : props;
      })
    },
    placeholderData,
  });
  return res;
}

And use it to get data from getServerSideProps/getStaticProps functions

const { data: article, isLoading, isFetching, isStale} = usePageData<BlogItemPageProps>();

4) Create OptimisticLink component

import NextLink, { LinkProps } from 'next/link';
import React, { AnchorHTMLAttributes, MouseEvent, PropsWithChildren } from 'react';
import { handleOptimisticNavigation } from 'next-optimistic-router';
import singletonRouter from 'next/router';

type NextLinkProps = PropsWithChildren<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
  LinkProps>

type Props = NextLinkProps & {
  placeholderData?: object;
}

export const Link = React.forwardRef<HTMLAnchorElement, Props>(function LinkComponent(props, ref) {
  const {
    placeholderData,
    onClick,
    href,
    children,
    ...restProps
  } = props;
  const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
    if (onClick) {
      onClick(e);
    }
    handleOptimisticNavigation({
      href,
      singletonRouter,
      withTrailingSlash: Boolean(process.env.__NEXT_TRAILING_SLASH),
    });
    window.placeholderData = placeholderData;
  }

  return (
    <NextLink
      onClick={handleClick}
      href={href}
      prefetch={false}
      ref={ref}
      {...restProps}
    >{children}</NextLink>
  )
});

Use OptimisticLink component like regular next/link component