Pay attention
This documentation is for the as yet unreleased version of effector Spacewatch 23.0.

Integrate Next.js with effector

To do this, we will use the native fork method to create a scope This hook does that and memoize our data. It also merges server and client states:

import { fork, Scope, serialize } from "effector";
import { useMemo } from "react";

let scope;

function initScope(initialData) {
  return fork({ values: initialData });
}

function initializeScope(preloadedData) {
  let _scope = scope ?? initScope(preloadedData);
  // After navigating to a page with an initial scope state, merge that state
  // with the current state in the scope, and create a new scope
  if (preloadedData && scope) {
    _scope = initScope({
      ...serialize(scope, { onlyChanges: true }),
      ...preloadedData,
    });
    // Reset the current scope
    scope = undefined;
  }
  // For SSG and SSR always create a new scope
  if (typeof window === "undefined") {
    return _scope;
  }
  // Create the scope once in the client
  if (!scope) {
    scope = _scope;
  }
  return _scope;
}

export function useScope(initialState): Scope {
  return useMemo(() => initializeScope(initialState), [initialState]);
}

In the app.tsx file, we used our useScope and wrap the application in a provider from effector-react/scope.

import { AppProps } from "next/app";
import { FC } from "react";
import { Provider } from "effector-react/scope";
import { fork, Scope, serialize } from "effector";
import { useScope } from "../useScope"; // use your path to file

const App: FC<AppProps<{ initialState: InitialStateType }>> = ({ Component, pageProps }) => {
  const scope = useScope(pageProps.initialState);

  return (
    <Provider key={scope?.graphite?.id || "0"} value={scope}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default App;

Next, you need to add .babelrc to the project root with the following content:

{
    "plugins": [
        [
            "effector/babel-plugin", {
            "reactSsr": true,
        }
        ]
    ]
}

You also need to add an alias to next.config.js to avoid build errors, when under the hood it imports, then cjs then mjs files.

config.resolve.alias.effector = path.resolve(__dirname, "./node_modules/effector/effector.cjs.js");
config.resolve.alias["effector-react/ssr"] = path.resolve(
  __dirname,
  "./node_modules/effector-react/ssr.js",
);
config.resolve.alias["effector-react/scope"] = path.resolve(
  __dirname,
  "./node_modules/effector-react/scope.js",
);
config.resolve.alias["effector-react"] = path.resolve(
  __dirname,
  "./node_modules/effector-react/ssr.js",
);

For the convenience of development, we add the following rules to our eslint:

module.exports = {
  root: true,
  extends: [
    "effector",
    "plugin:effector/react",
    "plugin:effector/scope",
    "plugin:effector/future",
    "plugin:effector/patronum",
    "plugin:effector/recommended",
  ],
  rules: {
    "no-restricted-imports": [
      "error",
      {
        paths: [
          {
            name: "effector-react",
            message: "Please import from 'effector-react/scope' instead.",
          },
        ],
      },
    ],
  },
};
Contributors