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

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>
  )
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 11
  • Comments: 23 (1 by maintainers)

Commits related to this issue

Most upvoted comments

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

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 Any estimate on when this change will be released in a new NPM version?

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…

@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.

@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!

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