How to reload a Next.js page's initial props without reloading the whole page
Tags: react, nextjs, javascript, typescript
Published: 2020-05-18 14:08:25

If you have made a Next.js page that loads data in its `getInitialProps` function, you may have wanted to reload it in order to fetch the newest data after making some mutating API call. Hard-reloading the page will reset the user's scroll position and take extra time, so clearly that is not the best solution. You could also make some `fetchData` function that gets called in getInitialProps, passes its data into the default value of a useState hook, and then gets updated by calling `fetchData` again followed by setting the state. But wouldn't it be convenient if you could just have a function that reloaded your initial props?
I made a higher order component to wrap Next.js pages that does exactly this. Here it is in TypeScript:
```typescript
import { NextPage, NextPageContext } from 'next';
import React, { useState, PropsWithChildren, ComponentType } from 'react';

export interface SoftReloadProps {
  /**
   * Reloads the page's initialProps without causing a real browser "hard" reload.
   */
  softReload(): void;
}

type OmitSoftReloadProps<T> = Omit<T, keyof SoftReloadProps>;
type SoftReloadablePageInitialProps<TProps> = OmitSoftReloadProps<TProps> & { context: NextPageContext };
export type NextPageWithInitialProps<P, IP = P> = NextPage<P, IP> & Required<Pick<NextPage<P, IP>, 'getInitialProps'>>;

/**
 * Removes never-used context values to reduce bloat. Context values may come from server but then
 * be used client-side because they are saved in initial props.
 */
function minifyContext(context: NextPageContext): NextPageContext {
  return { ...context, req: undefined, res: undefined };
}

const withSoftReload = <TProps extends SoftReloadProps>(
  Page: NextPageWithInitialProps<TProps, OmitSoftReloadProps<TProps>>
): NextPage<SoftReloadablePageInitialProps<TProps>> => {
  async function getInitialProps(ctx: NextPageContext): Promise<SoftReloadablePageInitialProps<TProps>> {
    return { context: minifyContext(ctx), ...(await Page.getInitialProps(ctx)) };
  }
  const omitContextFromProps = ({
    context,
    ...props
  }: SoftReloadablePageInitialProps<TProps>): OmitSoftReloadProps<TProps> => props as any;
  const NewPage: NextPageWithInitialProps<SoftReloadablePageInitialProps<TProps>> = props => {
    // set inner page initial props to wrapper initial props minus context
    const [initialProps, setInitialProps] = useState(omitContextFromProps(props));
    async function softReload() {
      setInitialProps({ children: null, ...(await Page.getInitialProps(props.context)) });
    }
    return (
      <Page
        {...(({ ...initialProps, softReload } as Omit<TProps, keyof SoftReloadProps> & SoftReloadProps) as TProps)}
      />
    );
  };
  NewPage.getInitialProps = getInitialProps;
  NewPage.displayName = `withSoftReload(${Page.displayName})`;
  return NewPage;
};

export default withSoftReload;
```
You can use this HOC like this in your pages:
```typescript
interface InitialProps {
  data: string;
}

const MyPage: NextPageWithInitialProps<InitialProps & SoftReloadProps, InitialProps> = ({ data, softReload }) => (
  <div>
    {data}
    <button onClick={softReload}>Refresh</button>
  </div>
);

MyPage.getInitialProps = async (ctx) => {
  // fetch data
};

export default withSoftReload(MyPage);

```
Hope you enjoy!