Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SSR] Recoil doesn't work server side with Next.js 13 #2082

Closed
leonbe02 opened this issue Oct 28, 2022 · 23 comments
Closed

[SSR] Recoil doesn't work server side with Next.js 13 #2082

leonbe02 opened this issue Oct 28, 2022 · 23 comments

Comments

@leonbe02
Copy link

leonbe02 commented Oct 28, 2022

The Gist

When using Recoil + Next.js 13, I'm encountering an error while pre-rendering the page server side. The app works as expected client side though so no issue there, just when Recoil is used during server side pre-render.

The Error

TypeError: batcher is not a function
    at MutableSnapshot.batchUpdates [as _batch] (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:3447:3)
    at eval (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:3981:12)
    at eval (webpack-internal:///(sc_client)/./app/RecoilProvider.tsx:17:9)
    at Snapshot.eval [as map] (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:3749:7)
    at freshSnapshot (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:3939:45)
    at initialStoreState (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:4397:20)
    at eval (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:4548:187)
    at useRefInitOnce (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:4078:19)
    at RecoilRoot_INTERNAL (webpack-internal:///(sc_client)/./node_modules/recoil/cjs/index.js:4548:19)
    at renderWithHooks (node_modules/next/dist/compiled/react-dom/cjs/react-dom-server.browser.development.js:7621:16)

The Environment

  • next: 13.0.0
  • react: 18.2.0
  • react-dom: 18.2.0
  • recoil: 0.7.6

The Code

// app/layout.tsx

'use client';

import RecoilProvider from './RecoilProvider';

export default function RootLayout(
  { children }: { children: JSX.Element }
) {
  return (
    <html lang="en-us">
      <head>
        <title>Next 13 + Recoil Example</title>
      </head>
      <body>
        <RecoilProvider locale="en-us">{children}</RecoilProvider>
      </body>
    </html>
  );
}
// app/atoms/i18n.tsx

'use client';

import { atom } from 'recoil';

export const locale = atom({
  key: 'locale',
  default: 'en-us',
});
// app/page.tsx

'use client';

import { useRecoilValue } from 'recoil';
import * as i18n from './atoms/i18n';

export default function Home() {
  const locale = useRecoilValue(i18n.locale);

  return (
    <main>
      <p>The locale: {locale}</p>
    </main>
  );
}
// app/RecoilProvider.tsx

'use client';

import { useCallback } from 'react';
import { RecoilRoot, SetRecoilState } from 'recoil';
import * as i18n from './atoms/i18n';

export default function RecoilProvider({
  locale,
  children,
}: {
  locale: string,
  children: JSX.Element,
}) {
  const initializeState = useCallback(({ set }: { set: SetRecoilState }) => {
    set(i18n.locale, locale);
  }, [locale]);

  return (
    <RecoilRoot initializeState={initializeState}>
      {children}
    </RecoilRoot>
  )
}
@shifenhutu
Copy link

same here

@drarmstr drarmstr added help wanted Extra attention is needed server rendering labels Oct 31, 2022
@drarmstr drarmstr changed the title Recoil doesn't work server side with Next.js 13 [SSR] Recoil doesn't work server side with Next.js 13 Oct 31, 2022
@Soumajit2004
Copy link

Soumajit2004 commented Nov 1, 2022

It works for me if i use "use client at top" but doesn't work when i use server components can anyone explain

If i use "use client" in layout.js does it means the child components will be rendered in client

@398noe
Copy link

398noe commented Nov 2, 2022

same for me :(

@tpatalas
Copy link

tpatalas commented Nov 2, 2022

Just finished migrating one of my projects to 13. This is what I found:

  • All work fine as long as components using Recoil hooks have the directive 'use client'. That includes all the child components.
  • Using the 'use client' directive in layout ONLY will fail to load as mentioned above.
  • Now fetching with atomEffect works without turning off SSR through the dynamic import module, but will fetch twice... except the one pre-fetching with useRecoilCallback.
  • Does not need to use the 'use client' directive to atoms/selectors/custom hook using useRecoilCallback if they are in separate files. It is fine if the client directive is properly used within the component.

All work is the same as Next12 on my side except for some non-Recoil-related errors, such as some 'Modules are not found.' This is a known issue, I believe.

Next13's app directory is far from ready to use, and I will skip using it until it becomes stable.

@leonbe02
Copy link
Author

leonbe02 commented Nov 2, 2022

Upon further investigation, the issue appears to be specific to using the initializeState property on RecoilRoot. If I remove the function that initializes the atom, the page renders server side just fine. When I pass in a function and attempt to initialize the state, I get the batcher is not a function error.

@leonbe02
Copy link
Author

leonbe02 commented Nov 3, 2022

I've pinpointed the issue I believe to the usage of unstable_batchedUpdates from react-dom: https://github.com/facebookexperimental/Recoil/blob/main/packages/recoil/core/Recoil_Batching.js#L14

Nextjs appears to use the ReactDOMServerRenderingStub during SSR which does not export the unstable_batchedUpdates function.
https://github.com/vercel/next.js/blob/canary/packages/next/build/webpack-config.ts#L1106-L1115

Is that batchedUpdates function needed during SSR for Recoil? With React 18+, I believe all setState operations are batched automatically so my guess is it's not needed at all if you're using the latest React.

When I remove the unstable_batchedUpdates import and the batcher(() => ...) function from that Recoil_Batching.js file, everything works fine

@kensoz
Copy link

kensoz commented Nov 8, 2022

same here in SSG

Gumichocopengin8 pushed a commit to Gumichocopengin8/Recoil that referenced this issue Nov 10, 2022
…g SSR (facebookexperimental#2086)

Summary:
Resolves facebookexperimental#2082

Pull Request resolved: facebookexperimental#2086

Reviewed By: wd-fb

Differential Revision: D41054948

Pulled By: drarmstr

fbshipit-source-id: eb421deb5a84e9ff9390a1a8d88b28c720765cb1
@leonbe02
Copy link
Author

leonbe02 commented Dec 5, 2022

@drarmstr Any estimate on when this change will be released in a new NPM version?

@devcaeg
Copy link

devcaeg commented Dec 28, 2022

Any news about not being able to initialize Recoil with data obtained in SSR in Next.js 13?

@andreisoare
Copy link

@drarmstr would you be able to help make a release that includes this fix? Still blocked by this. Or is there someone else who can help? Thank you!

@rootical
Copy link

rootical commented Feb 8, 2023

Another vote for a release...

@leonbe02
Copy link
Author

Has this project died or something? There hasn't been a new version released since October of last year. I know Facebook had layoffs but I didn't realize they laid off the entire team behind Recoil...

@jephjohnson
Copy link

Any updates on this? Same Issues

@Mng12345
Copy link

Same for me. Does it mean that the component has a head line with "use client" would not be rendered in the server side?

@RizqiSyahrendra
Copy link

for this issue, I ended up manually waiting the client ready before rendering from recoil state, I did this because the content I was rendering is not required to be SSR

snipershooter0701 pushed a commit to snipershooter0701/Recoil that referenced this issue Mar 5, 2023
…g SSR (#2086)

Summary:
Resolves facebookexperimental/Recoil#2082

Pull Request resolved: facebookexperimental/Recoil#2086

Reviewed By: wd-fb

Differential Revision: D41054948

Pulled By: drarmstr

fbshipit-source-id: eb421deb5a84e9ff9390a1a8d88b28c720765cb1
hyochan added a commit to hyochan/github-stats that referenced this issue Mar 11, 2023
@khashayarghajar
Copy link

any news ?

@DjibrilM
Copy link

Just finished migrating one of my projects to 13. This is what I found:

  • All work fine as long as components using Recoil hooks have the directive 'use client'. That includes all the child components.
  • Using the 'use client' directive in layout ONLY will fail to load as mentioned above.
  • Now fetching with atomEffect works without turning off SSR through the dynamic import module, but will fetch twice... except the one pre-fetching with useRecoilCallback.
  • Does not need to use the 'use client' directive to atoms/selectors/custom hook using useRecoilCallback if they are in separate files. It is fine if the client directive is properly used within the component.

All work is the same as Next12 on my side except for some non-Recoil-related errors, such as some 'Modules are not found.' This is a known issue, I believe.

Next13's app directory is far from ready to use, and I will skip using it until it becomes stable.

then why should we use Nextjs if we shall end up turning everything into a client component?

@drarmstr drarmstr removed the help wanted Extra attention is needed label Apr 24, 2023
@drarmstr
Copy link
Contributor

Fixed with #2086 and released with 0.7.7

@elmcapp
Copy link

elmcapp commented May 17, 2023

This is not an issue. The way that Next.js works is server-side first. You can't just wrap the app in the <RecoilRoot> You do have to have use the follow "use client" however you have to put it in a wrapper component if you are using the app directory. According to Next.js state including state management must be handled on the client as there is no way to have state on the server side. Below is a working example that works for me

  1. Create a wrapper file called RecoilRootWrapper.js
"use client";

import React from "react";
import { RecoilRoot } from "recoil";

function RecoilRootWrapper({ children }) {
  return <RecoilRoot >{children}</RecoilRoot>;
}

export default RecoilRootWrapper;

  1. In the app directory edit the layout.tsx or layout.js file
- src/
  - app/
    - pages.tsx
    - layout.tsx

layout.tsx file should look like this:

import RecoilRootWrapper from "@/wrappers/RecoilRootWrapper";
import "./globals.css";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
 title: "Create Next App",
 description: "Generated by create next app",
};

export default function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
   <html lang="en">
     <body className={inter.className}>
       <RecoilRootWrapper>{children}</RecoilRootWrapper>
     </body>
   </html>
 );
}

By doing this we put the Recoil provider on the client (browser) and anything that is not marked with "use client" will be rendered as server side. You can not add "use client" to the layout.tsx or layout.js, page.tsx or page.js files

@MattSteedman
Copy link

This is not an issue. The way that Next.js works is server-side first. You can't just wrap the app in the <RecoilRoot> You do have to have use the follow "use client" however you have to put it in a wrapper component if you are using the app directory. According to Next.js state including state management must be handled on the client as there is no way to have state on the server side. Below is a working example that works for me

  1. Create a wrapper file called RecoilRootWrapper.js
"use client";

import React from "react";
import { RecoilRoot } from "recoil";

function RecoilRootWrapper({ children }) {
  return <RecoilRoot >{children}</RecoilRoot>;
}

export default RecoilRootWrapper;
  1. In the app directory edit the layout.tsx or layout.js file
- src/
  - app/
    - pages.tsx
    - layout.tsx

layout.tsx file should look like this:

import RecoilRootWrapper from "@/wrappers/RecoilRootWrapper";
import "./globals.css";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
 title: "Create Next App",
 description: "Generated by create next app",
};

export default function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
   <html lang="en">
     <body className={inter.className}>
       <RecoilRootWrapper>{children}</RecoilRootWrapper>
     </body>
   </html>
 );
}

By doing this we put the Recoil provider on the client (browser) and anything that is not marked with "use client" will be rendered as server side. You can not add "use client" to the layout.tsx or layout.js, page.tsx or page.js files

Although this might work as a hack there are some files or compomponents you want server-side so wont this wrapper essentially put all those {children} in the clientside in your layout.tsx file

@elmcapp
Copy link

elmcapp commented Jun 8, 2023

@MattSteedman Nope even if you put it as a wrapper any child components will still be server unless you use "use client" in each component. This is not a hack or workaround. Recoil is state management tools and anything that use state must be rendered on the client.

@Yedidya10
Copy link

Yedidya10 commented Aug 12, 2023

Hi, There is another issue happening because of SSR

If I want to use the effects property, even though I specify 'use client' in the component I wrap in <RecoilRoot>, I still get an error:

Unhandled Runtime Error
Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.

Is there a solution for this, or is there a PR about it?

import { AtomEffect, atom } from 'recoil'

export type ThemeMode = 'light' | 'dark'

const localStorageEffect =
  (key: string): AtomEffect<ThemeMode> =>
  ({ setSelf, onSet }) => {
    // Retrieve the value stored at the specified key
    const stored = localStorage.getItem(key)
    // Check if the value exists and is light or dark
    if (stored === 'dark' || stored === 'light') {
      // If the value is valid, the call the provided function setSelf which initializes the atom value
      setSelf(stored)
    }
    // Creates the callback triggered when the atom is changed
    onSet((value, _, isReset) => {
      if (isReset) {
        // If atom has been reset then remove it from local storage
        localStorage.removeItem(key)
      } else {
        // If value has changed then store the value in local storage
        localStorage.setItem(key, value || _) // the || is a fail-safe if for any reason value is null the value will revert to default
      }
    })
  }

const themeModeState = atom<ThemeMode>({
  key: 'themeModeState',
  default: 'light',
  effects: [localStorageEffect('themeMode')],
})

export default themeModeState

It should be noted that in (RootLayout) layout.tsx I do not use 'use client' because I use a server function:

async function getSession(cookie: string): Promise<Session> {
   const response = await fetch(`${process.env.NEXTAUTH_URL}/api/auth/session`, {
     headers: {
       cookie,
     },
   })

   const session = await response.json()

   return Object.keys(session).length > 0 ? session : null
}

There is no recoil component on the layout.tsx page

@bennidhamma
Copy link

bennidhamma commented Nov 4, 2023

Is there any way to share recoil state across multiple recoil roots? This suggests no:

Multiple RecoilRoot's may co-exist and represent independent providers/stores of atom state; atoms will have distinct values within each root.

https://recoiljs.org/docs/api-reference/core/RecoilRoot/

Does this mean recoil state can only apply to a single client subtree of a next app? My use case is that I have a SSR app, with different tree nodes rendered on client using the 'use client' directive.

Server Component
 |     |     |
 A     B     C

Where child components A and C are client components. Is it possible to share recoil state between A and C? it seems like that could theoretically work - they are both in the same browser process after all....

(this seems like a pretty common use case for state - updating a navigation sidebar if a item is consumed / interacted, or a shopping cart count, etc....)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.