@effector/next

中文文档
0.7.2发布于 2025/09/07MITTypeScriptESM + CJS零依赖近 30 天下载 2.5万

效果器 + Next.js 最小相容层

中文使用文档
文档目录15 节
  1. 安装
  2. 使用量
  3. 电离层
  4. 效果器 Eslint 插件
  5. 页面路由器 使用量
  6. 应用路由器 使用量
  7. 开发工具集成
  8. 重要的说明
  9. 使用 Pages 路由器和 App 路由器
  10. App 路由器架构边框
  11. 非连续数值
  12. 效应器 serialize: "ignore" 不建议
  13. 不使用 $store.watch 调试请求
  14. 组合中的无害环境管理依赖性和库重复
  15. 释放过程

下个.js的装订

这是效果器 + Next.js 的最小兼容层 - 它只提供一个特殊的 EffectorNext 提供方组件,它允许充分杠杆化效果器的分叉 API,同时处理一些 特别 next.js SSSR和SSG的部分流量.

安装

npm add effector effector-react @effector/next

还有,你可以使用雅恩或 菲律宾 以安装依赖.

使用量

您可以在“% 1” 中找到示例应用程序 apps/playground-app - 它包含各种情况下的App路由器和Pages路由器使用例.

电离层

要在网络边界之间序列化和传输效应存储状态,所有存储都必须有一个稳定识别符- sid.

Sid通过内置的 Babel 插件或我们实验的 SWC 插件自动添加.

贝贝-插剂

Effector 带有内置的官方 Babel 插件, 您可以用于 SID :

{
  "presets": ["next/babel"],
  "plugins": ["effector/babel-plugin"]
}

请注意 你没必要 页:1 reactSsr 选项 @effector/next编辑 reactSsr 是一个遗留的API,在Effector的最新发行中被贬值.

读取文件

SWC 插件

插件有一个很棒的 SWC 端口, 请检查:

读取效果器 SWC 插件文档

效果器 Eslint 插件

重新建议安装官员 效果器 Eslint 插件 与 React Preset 一起,它会帮助您遵循最佳做法并避免常见的错误,就像总是使用 useUnit

安装

见 在此安装指南。 。 。 .

设置

.slintrc (英语)

{
  "plugins": ["effector"],
  "extends": ["plugin:effector/react"]
}

别忘了 useUnit 向所有人开放 effector 单位

在 SSR 应用中,所有效果器的单位都需要被“被”到当前 Scope,这意味着你应该总是使用 useUnit 在组件中:

import { useUnit } from "effector-react"
import { eventTriggered, effectFx, $store } from "./model"

export function Component() {
 const {
  value,
  trigger,
  callEffect
 } = useUnit({
   value: $store,
   trigger: eventTriggered,
   callEffect: effectFx
 })

 // rest of the components code
}

您可以找到关于 useUnit 这里。 。 。 .

官员 效果器 Eslint 插件 使用 React Preset 会帮助您遵循此规则 .

页面路由器 使用量

在下一个.js之前 13.4.0 Pages路由器是构建Next.js应用程序的主要途径. 页面路由器 将支持多个 Next.js 主要更新。 。 。 .

这个 @effector/next 完全支持退出框的 Page 路由器.

1. 下一个提供商设置

将提供者添加到 pages/_app.tsx 并为其提供服务器侧 values

import { EffectorNext } from "@effector/next";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <main>
      <EffectorNext values={pageProps.values}>
        <Layout>
          <Component />
        </Layout>
      </EffectorNext>
    </main>
  );
}

请注意 EffectorNext 应通过道具获得序列化范围值.

2. 服务器侧计算

使用叉 API 在服务器处理器中开始计算 。 工作流程对于Next.js的所有服务器侧功能都是相同的.

getStaticProps 实例

import { fork, allSettled, serialize } from "effector";

import { pageStarted } from "../src/my-page-model";

export async function getStaticProps() {
  const scope = fork();

  await allSettled(pageStarted, { scope, params });

  return {
    props: {
      // notice serialized effector's scope here!
      values: serialize(scope),
    },
  };
}

通知,序列化范围值是通过下列方式提供的: values 道具,用于 _app.tsx 用于向 EffectorNext。 。 。 . 您可以选择一些道具名称来连接服务器处理器和客户端道具 _app.tsx。 。 。 .

3. Next.js Pages 路由器 API 使用

有一些特殊的API,它们以Pages路由器模式工作.

API 详情请见相关 Next.js 文件

getServerSideProps 与 { notFound: true }

您可以返回一个特殊对象 { notFound: true } 形状以命令 Next.js 渲染 常见 404页

import { fork, allSettled, serialize } from "effector";

import { pageStarted, $isNotFound } from "../src/my-page-model";

export async function getStaticProps() {
  const scope = fork();

  await allSettled(pageStarted, { scope, params });

  if (scope.getState($isNotFound)) {
    // If, after all computations there is still something missing,
    // and user is meant to see the 404

    return {
      notFound: true,
    }
  }

  return {
    props: {
      // notice serialized effector's scope here!
      values: serialize(scope),
    },
  };
}

这个 scope.getState 模式适用于其他任何 Next.js Pages 路由器服务器侧API,以及App路由器 API(见下文)

都准备好了 只需在组件代码中任意使用效果器的单位 useUnit 从 effector-react。 。 。 .

另见 nextjs-effector 软件包( r)是啊,命名 Next.js 相关的软件包 是一种比较),它提供了更好的DX至Pages路由器的使用,并被建在顶端 @effector/next。 。 。 .

应用路由器 使用量

App 路由器是使用 React 的最新特性构建 Next.js 应用程序的新范式,它宣布自 Next.js 以来稳定 13.4.0。 。 。 .

这个 @effector/next 完全支持 App 路由器退出框.

1. 根版式中的设置提供者

使用树上任何地方带有效果单位的客户端组件 - 添加 EffectorNext 服务商在您的 根布局

使用时 多个根布局 - 他们每个人都应该拥有 EffectorNext 供养.

// app/layout.tsx
import { EffectorNext } from "@effector/next";

export function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <EffectorNext>{/* rest of the components tree */}</EffectorNext>
      </body>
    </html>
  );
}

QQ 这将允许树上任何地方的任何组件访问任何 Effector 商店, 无论该页面是否执行服务器侧计算

2. 服务器侧计算

服务器以类似方式计算 pages 目录,但在服务器组件内 app 页面.

在此情况下,您需要添加 EffectorNext 此服务器组件的树的提供者, 并为其提供序列化范围 .

// app/some-path/page.tsx
import { EffectorNext } from "@effector/next";

export default async function Page() {
  const scope = fork();

  await allSettled(pageStarted, { scope, params });

  const values = serialize(scope);

  return (
    <EffectorNext values={values}>
      {/* rest of the components tree */}
    </EffectorNext>
  );
}

这将自动渲染 这个子树 并自动“水合物” 客户范围 带有新值,一旦在浏览器中提供此更新.

3. 下一步.js API的使用

通过叉 API 开始计算并使用 scope.getState 以提取存储中的数据并将其提供给下一个。 js API 喜欢 generateStaticParams

应当指出 getState 使用在生产代码中通常是不可取的,除了需要将效果器与一些外部的东西或api连接起来的情况,Next.js在这里就是这种情况.

generateStaticParams 实例

API 详情请参见 Next.js 文件

// app/blog/[slug]/page.tsx
import { fork, allSettled, serialize } from "effector"

import { blogPostOpened, blogPostsStarted, $postsList } from "@app/features/blog"

// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
  const scope = fork();

  await allSettled(blogPostsStarted, { scope });

  const posts = scope.getState($postsList); // { name: string; id: string; }[]

  // map to match `[slug]` param naming
  return posts.map(({ id }) => ({ slug: id }));
}

// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default async function Page({ params }: { params: { slug: string } }) {
  const { slug } = params;

  const scope = fork();

  await allSettled(blogPostOpened, { scope, params: { id: slug } });

  const values = serialize(scope);

  return (
    <EffectorNext values={values}>
      {/* rest of the components tree */}
    </EffectorNext>
  );
}
notFound 财务报告和审定财务报表 redirect 实例

很少有特殊功能,当从服务器组件内部调用时,会丢出一个特殊的例外,它会命令Next.js在某个地方执行重定向. 这个 notFound 函数将重定向到 notFound.tsx 查看当前页面.

API 详情请参见 Next.js 文件

// app/blog/[slug]/page.tsx
import { fork, allSettled, serialize } from "effector"
import { notFound } from "next/navigation"

import { blogPostOpened, $currentPost } from "@app/features/blog"

export default async function Page({ params }: { params: { slug: string } }) {
  const { slug } = params;

  const scope = fork();

  await allSettled(blogPostOpened, { scope, params: { id: slug } });

  if (!scope.getState($currentPost)) {
    // If there is no current post available after all computations
    // Next.js redirect to `notFound.tsx` is triggered
    notFound()
  }

  const values = serialize(scope);

  return (
    <EffectorNext values={values}>
      {/* rest of the components tree */}
    </EffectorNext>
  );
}

QQ 对 Next.js 任何其他特殊功能都一样: 如果您需要访问客户端组件以外的存储值, 您需要使用 scope.getState。 。 。 .

Server Actions 实例
  1. 必须创建服务器动作 照旧。 。 。 .
// src/feature/action.ts
"use server"

export const myAction = async () => doServerOnlyStuff()
  1. 然后服务器动作可以在 Effector 模型中用作一种 效果处理器
// src/feature/model.ts
import { createEffect } from "effector"
import { myAction } from "./action.ts"

const myActionFx = createEffect(myAction)

QQ 这种效果可以在任何地方调用

就是这样.

见 apps/playground-app/src/features/server-action 为了活的榜样.

别忘了 use client 客户端组件:

刚刚 写入效果器的模型, 一如既往 在组件代码的任意位置使用效果器的单位 通过 useUnit 从 effector-react -但别忘了 要用钩子你需要 "use client" 指令.

// src/features/blog/post.view.tsx
// you should use `use client`, if your component uses any hooks
"use client"
import { useUnit } from "effector-react"
import { Title, Content } from "@app/shared/ui-kit"
import { PostCommentsView } from "@app/features/comments"

import { $currentPost } from "./model.ts"

export function CurrentBlogPost() {
  const post = useUnit($currentPost)

  return (
    <article>
      <Title{post.title}</Title>
      <Content>
        {post.content}
      </Content>
      <PostCommentsView postId={post.id} />
    </article>
  )
}

开发工具集成

多数 effector 非工具选项需要直接访问 scope 该应用程序. 在客户端,您可以通过 getClientScope 函数,将返回 Scope 在浏览器和 null 在服务器上.

这里举几个例子: @effector/redux-devtools-adapter 一体化.

Pages 路由器 Dev- Tools 示例

如果设置了 Pages 路由器,则必须在 自定义 App 组件文件 (pages/ app.tsx)。 。 。 .

// pages/_app.tsx
import type { AppProps } from "next/app";
import { EffectorNext, getClientScope } from "@effector/next";
import { attachReduxDevTools } from "@effector/redux-devtools-adapter";

const clientScope = getClientScope();

if (clientScope) {
  /**
   * Notice, that we need to check for the client scope first
   *
   * It will be `null` at the server
   */
  attachReduxDevTools({
    scope: clientScope,
    name: "playground-app",
    trace: true,
  });
}

function App({
  Component,
  pageProps,
}: AppProps<{ values: Record<string, unknown> }>) {
  const { values } = pageProps;

  return (
    <EffectorNext values={values}>
      <Component />
    </EffectorNext>
  );
}

export default App;

App 路由器 Dev- Tools 示例

如果设置了 App 路由器- dev-tool,则必须在 根布局 - 这样,dev-tools集成将会为应用程序的所有页面工作.

创建客户端组件

因为Redux Dev-Tools是客户端的东西 - 我们需要准备它作为一个客户端组件.

// src/shared/redux-dev-tools-provider.tsx
"use client";

import { getClientScope } from "@effector/next";
import { attachReduxDevTools } from "@effector/redux-devtools-adapter";

const clientScope = getClientScope();

if (clientScope) {
  /**
   * Notice, that we need to check for the client scope first
   *
   * It will be `null` at the server
   */
  attachReduxDevTools({
    scope: clientScope,
    name: "playground-app-app-router",
    trace: true,
  });
}

export function ReduxDevToolsAdapter({
  children,
}: {
  children?: React.ReactNode;
}) {
  return <>{children}</>;
}
将此组件添加到根版式
// app/layout.tsx
import { EffectorNext } from "@effector/next";

import { ReduxDevToolsAdapter } from "app-root/shared/redux-dev-tools-provider"

export function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ReduxDevToolsAdapter />
        <EffectorNext>{children}</EffectorNext>
      </body>
    </html>
  );
}

重要的说明

Next.js的行为有一些特殊的细微差别,你需要考虑.

使用 Pages 路由器和 App 路由器

要注意Next.js基本上为Pages和App路由器模式构建了两个不同的应用程序捆绑.

之间 Pages Router 财务报告和审定财务报表 App Router 总是通过整页重新装入来进行 吹走任何客户端状态 . 这已经足够逐渐了 App Router 但是,如果想要最佳的经验,你应该避免在同一应用程序中混合不同的路由器模式。 用户经常在之间转换的页面最好移动到 App Router 一起来.

新的应用程序应该总是以 App Router 开始,因为它是现在构建 Next.js 应用程序的主要途径.

App 路由器架构边框

App路由器架构有一个已知的警告,到目前为止适用于 effector 财务报告和审定财务报表 @effector/next 还有:

布局组件,页面组件和任何被嵌入的React Server组件在服务器渲染过程中无法互相了解.

QQ 表示如果存在某种效果器的存储器, 用于布局组件, 然后在渲染 Page 组件时更新 - 在客户端浏览器中, 可能有一个布局组件可见的布局闪烁器 .

它发生,因为在服务器上, 到Page 组件渲染的时刻,它的布局 已经提供了 它在反应中的一部分,包括它中的所有组件,它们会在渲染时使用存储状态. 因此,如果这种商店后来在Page 组件-客户端的布局compoment中被更改,则会在客户端被水分化后"看到"并对这一变化作出反应.

你可以在例子中复制这个行为 playground-app,通过修改 $counter 存储,用于控制在普通状态下渲染的导航栏中的计数器按钮状态 layout.tsx,在某些 page.tsx 组件.

导航栏的计数按钮将首先可见 0 (因为它是一个状态 $counter 在布局渲染时),然后将更改为在布局中设定的值 page.tsx 调制.

非连续数值

如果您拥有包含无法安全传递的值的 Effector 存储器 JSON.stringify,您会遇到将值传递给客户端的问题.

在这种情况下,必须通过商店设置指定自定义序列化规则.

示例

Date 对象默认会被序列化为 ISO- string,但不会被解析回 Date 对象通过 JSON.parse。 。 。 . 我们可以通过提供定制序列化规则来修复它.

const $date = createStore<null | Date>(null, {
  serialize: {
    write: (dateOrNull) => (dateOrNull ? dateOrNull.toISOString() : dateOrNull),
    read: (isoStringOrNull) =>
      isoStringOrNull ? new Date(isoStringOrNull) : isoStringOrNull,
  },
});

关于自定义序列化特性的文档

Next.js 网络(和序列化)边界被放置在任何客户端组件渲染之前,所以任何存储都使用 serialize: "ignore" 设置将总是使用默认值来制作,这可能导致混淆结果.

示例

// some-module.ts
const $serverOnlyValue = createStore(null, { serialize: "ignore" })

// $someValue is a derived store - such stores are never included in effector's serialization,
// because it is always possible to safely recalculate them from parent stores
//
// But in this case, combined with `serialize: "ignore"` on parent store, it will lead to confusing result
export const $someValue = $serverOnlyValue.map(value => value ? extractSomeSafeForClientData(value) : null)

// some-component

export function Component() {
 const value = useUnit($someValue)

 return value ? <>{value}<> : <>No value</>
}

// pages/some-page
export async function getServerSideProps(req) {
 const scope = fork()

  await allSettled(appStarted, { scope, params: req })

  // `scope.getState($serverOnlyValue)` is not null at this point
  // as well as `scope.getState($someValue)` is correctly calculated here

  // Next.js network boundary happens here and is separated from rendering components at the server
  return {
   props: {
    values: serialize(scope)
    // `scope.getState($serverOnlyValue)` is stripped off here, it's value will be default one :(
    // And `$someValue` is also calculated as if `$serverOnlyValue` is still null
    //
    // As a result - `Component` will always render "No value"
   }
  }
}

工作周边

你可以通过使用另一个非衍生商店来绕过它,这些商店将包含在效果器的序列中.

// some-module.ts
const $serverOnlyValue = createStore(null, { serialize: "ignore" })

// $someValue is non-derived and will be included in serialization, if changed
export const $someValue = createStore(null)

sample({
 clock: $serverOnlyValue,
 fn: value => value ? extractSomeSafeForClientData(value) : null,
 target: $someValue,
})

// some-component

export function Component() {
 const value = useUnit($someValue)

 return value ? <>{value}<> : <>No value</>
}

// pages/some-page
export async function getServerSideProps(req) {
 const scope = fork()

  await allSettled(appStarted, { scope, params: req })

  // `scope.getState($serverOnlyValue)` is not null at this point
  // as well as `scope.getState($someValue)` is correctly calculated here

  // Next.js network boundary happens here and is separated from rendering components at the server
  return {
   props: {
    values: serialize(scope)
    // `scope.getState($serverOnlyValue)` is stripped off here, it's value will be default one :(
    // But since `$someValue` is also non-derived store and it was changed on `$serverOnlyValue` update
    // it will be included in `values`
    //
    // As a result - `Component` will render `value` properly
   }
  }
}

不使用 $store.watch 调试请求

这个 $store.watch 在类似SPA的环境中加速调试的方法很有效,因为在这种情况下,效果器应用程序通常不运行 Scope。 。 。 . 然而,Next.js则不是这样,它作为SSSR应用程序运行——服务器和客户端上都有"范围"实例,应用程序的实际状态也存储在"范围"中. 您可以找到更多细节 在本条中。 。 。 .

问题是 $store.watch(cb) 呼叫触发器 cb 随即随着目前这个商店的"非范围"状态——由于历史原因,它以这种方式工作,因为表API是在"范围"和SSSR支持之前创建的.

您可以在本篇文章中找到详细的解释: https://wisease.pages.dev/magazine/watch calls (英语)

组合中的无害环境管理依赖性和库重复

自下届起.js 12 无害环境管理进口优先于共同JS进口。虽然CJS只支持依赖关系,但不建议使用它们.

它可能导致在捆绑中重复图书馆的例子,如果是 @effector/next 或者说 effector 导致奇怪的错误, 如缺少上下文提供者错误 .

您可以在捆绑中自动检查库复制件 分镜. 科技 网络包装插件, 具有特殊规则的。 。 。 .

您也可以手动通过 Debug -> Sources -> Webpack -> _N_E -> node_modules 选项卡。 复制的模块将在此显示 mjs 财务报告和审定财务报表 cjs 类型.

image

释放过程

  1. 看看这个 释放草稿。 。 。 .
  2. 所有公关应当有正确的标签和有用的标题. 你们可以 在此审查可用的标签。 。 。 .
  3. 更新 PR 和标题的标签, 下次 手动运行发行起草器动作 重新生成释放草稿.
  4. 审查新版本并按"Publish"
  5. 如果需要检查“ 此发行的创意讨论”

版本与发布记录

@effector/next 共发布 16 个版本,首次发布于 2023/04/03;过去 12 个月发布了 0 个版本。

版本发布日期距今
0.7.2latest2025/09/071.2 年前
0.7.12025/03/271.6 年前
0.7.02023/12/022 年前
0.6.12023/09/163 年前
0.6.02023/09/123 年前
0.5.12023/06/213 年前

关于 @effector/next 的常见问题

@effector/next 是什么?

效果器 + Next.js 最小相容层

如何安装 @effector/next?

在项目目录运行 npm install @effector/next,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.2.0 || ^19.0.0)、effector(^22.8.6 || ^23.0.0)、effector-react(^22.5.4 || ^23.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effector/next 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effector/next 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@effector/next 可以商用吗?

@effector/next 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effector/next 的下载量有多少?

根据 npm 官方下载统计,@effector/next 在 2026/10/13 – 2026/11/11 这 30 天内被下载 25,035 次,日均约 835 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effector/next 还在维护吗?

最新版本 0.7.2 发布于 2025/09/07(1.2 年前);至今共发布 16 个版本,其中过去 12 个月发布了 0 个;首次发布于 2023/04/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。