kirill-konshin / next-redux-wrapper

Redux wrapper for Next.js
MIT License
2.67k stars 265 forks source link

SSR Auth, fetching and setting auth user to the state inside App.getInitialProps causes an error when navigating between pages: Warning: Cannot update a component X while rendering a different component (`App`). #522

Open its-monotype opened 1 year ago

its-monotype commented 1 year ago

When I implemented the logic of fetching the authenticated user and setting it to the Redux state inside the App.getInitialProps method and accessing that user from the state within any Next.js page, I encountered an error when navigating between pages.

Warning: Cannot update a component (`HomeScreen`) while rendering a different component (`App`).
...
export const HomeScreen = () => {
  const user = useAppSelector(selectCurrentUser);
  ...

Here is the problem. Inside App.getInitialProps I'm fetching the API endpoint using RTK Query to retrieve the auth user from the cookie and when the query is fulfilled user adds to the state. And that causes that error in the console.

// _app.tsx

import '@/assets/styles/globals.scss';

import type { NextPage } from 'next';
import type { AppContext, AppProps } from 'next/app';
import { Provider } from 'react-redux';

import { wrapper } from '@/store';
import { me } from '@/store/auth/auth.api';

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: React.ReactElement) => React.ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};

function App({ Component, ...rest }: AppPropsWithLayout) {
  const {
    store,
    props: { pageProps },
  } = wrapper.useWrappedStore(rest);

  const getLayout = Component.getLayout ?? ((page) => page);

  return (
    <Provider store={store}>{getLayout(<Component {...pageProps} />)}</Provider>
  );
}

App.getInitialProps = wrapper.getInitialAppProps(
  (store) =>
    async ({ ctx, Component }: AppContext) => {
      // That string causes the error
      await store.dispatch(me.initiate());
      return {
        pageProps: {
          ...(Component.getInitialProps
            ? await Component.getInitialProps({ ...ctx, store })
            : {}),
        },
      };
    },
);

export default App;

The query that I initiate inside App.getInitialProps:

me: builder.query<User, void>({
      query: () => ({
        url: 'auth/me',
        method: 'GET',
      }),
      async onQueryStarted(_args, { dispatch, queryFulfilled }) {
        try {
          const { data } = await queryFulfilled;
          dispatch(setCredentials({ user: data }));
        } catch (err) {
          // Do nothing
        }
      },
    }),
kirill-konshin commented 1 year ago

Will be fixed in #523 but will require some code changes.

browynlouis commented 1 year ago

I am having same issue but this happens when I run the query in getServerSideProps and then call the rtkQuery hook inside any component or page, then I get the above error.

But it appears just once and no more.

browynlouis commented 1 year ago

Plus I am trying to achieve this but it doesn't work, because I need the context in the axiosbaseQuery I created and calling the endpoint in the getInitialProps of the app is out of the provider

PaulSinghDev commented 1 year ago

Just chiming in that this issue seems to occur when using RTK's dispatcher within a NextJS SSR block. I have a simple app skeleton that I am building out. It is using the SSR block to set the nav links into the state depending on the URL parameters. There is nowhere in the app's components where the store is changed other than with an onClick handler.

When I transition from the first page to the next this warning appears. The only way I can make it go away is by removing any references to the state selector for example, the NavLinks component which is using useAppSelect to get the nav links from my state/store.

Example SSR Block:

export const getServerSideProps = wrapper.getServerSideProps(
  (store) => async () => {
    // Set the nav first so we can access on the 404 page
    store.dispatch(
      setNavigation({
        isRestaurant: false,
        navLinks: genericNavLinks.links,
        isOpen: false,
      })
    );

    // Check we have page data
    const pageData = pages.find((page) => page.slug === "/");
    if (!pageData) return { notFound: true };
    return { props: { ...pageData } };
  }
);

The NavDrawer component (has the dispatch):

const NavDrawer: React.FC<NavDrawerProps> = ({ testId, id, style }) => {
  const { isOpen } = useAppSelector(selectNavigation);
  const dispatch = useAppDispatch();

  const handleNavClose: React.MouseEventHandler<HTMLButtonElement> = () =>
    dispatch(setNavigationOpen(false));

  return isOpen ? (
    <>
      <Underlay onClick={handleNavClose} isActive={true} />
      <div
        data-testid={testId}
        style={style}
        className={styles.wrapper}
        id={id}
      >
        <NavDrawerHeader />
        <NavLinks />
        <NavDrawerFooter />
      </div>
    </>
  ) : (
    <MobileNavOpenButton />
  );
};

The NavLinks component (If I remove the selector from here the warning goes away):


const NavLinks: React.FC<NavLinksProps> = ({ testId, id }) => {
  const { navLinks } = useAppSelector(selectNavigation);
  return (
    <div className={styles.wrapper}>
      <ul role="list" data-testid={testId} className={styles.list} id={id}>
        {navLinks.map((link) => (
          <NavLink key={link.id} link={link} />
        ))}
      </ul>
    </div>
  );
};

This has been doing my head in for a few days now, any advice?

PaulSinghDev commented 1 year ago

Ok, so I have managed to find a solution in my specific situation. I think this is something which will be fixed in https://github.com/kirill-konshin/next-redux-wrapper/pull/523 if I read both the issue and the solution properly.

In my case it seemed to be the case that the <NavDrawer /> component, and its children, were still rendering at the point the SSR block was dispatching its update to the store.

This meant that the isOpen value was true and an open nav drawer was rendering. Then the isOpen value was changed to false (via the SSR block) before it finished. This would stand to reason with the information in the error and why it only happened on the first transition.

From what I am making of it, the issue was: Initial state (isOpen: false) > SSR dispatch (isOpen: false) > Nav Drawer opened (isOpen: true) > Click Anchor (isOpen: true) > Trigger hydration (isOpen: true) > Hydration changes state (isOpen: false) > Component is stale (isOpen: true) > Re-render DOM with new state (isOpen: false) > Initial state not set again

For me to solve this I altered my <AnchorLink/> component to take an option callback and placed the dispatch in there. This has removed the error entirely, presumingly because the state is updated before the transition to a new page begins, thus has time to complete before hitting the SSR block.

If anyone else has a better idea of why this approach worked I would definitely love to hear :)

EDIT:

Adding on to this it seems that allowing the hydrate block to change the actual nav links which are rendered will cause this issue to persist. I presume the issue is stemming from the fact that the array of nav links has been changed into a proxied array which has also proxied each of the objects within it?

    // No issue
    builder.addCase(hydrate, (state, action) => {
      state.isOpen = action.payload.navigation.isOpen;
      state.isRestaurant = action.payload.navigation.isRestaurant;
      state.restaurant = action.payload.navigation.restaurant;

      return state;
    });

    // Issue
    builder.addCase(hydrate, (state, action) => {
      state.isOpen = action.payload.navigation.isOpen;
      state.isRestaurant = action.payload.navigation.isRestaurant;
      state.restaurant = action.payload.navigation.restaurant;
      // Problematic line
      state.navLinks = action.payload.navigation.navLinks;

      return state;
    });

EDIT 2:

The solution above is not a solution (whoops) turns out that by not including the links in the hydration update the menu is not updated when it should be and the first set of links the user acquires is the only one they will ever see :/

EDIT 3:

Sorry for all the spam. Just want to chime in that upgrading to the latest pre release branch has removed the issue for me while allowing the nav links to be updated on the hydration action