下个.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 端口, 请检查:
效果器 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路由器模式工作.
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 实例
// 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 查看当前页面.
// 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 实例
- 必须创建服务器动作 照旧。 。 。 .
// src/feature/action.ts
"use server"
export const myAction = async () => doServerOnlyStuff()
- 然后服务器动作可以在 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,
},
});
效应器 serialize: "ignore" 不建议
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 类型.
释放过程
- 看看这个 释放草稿。 。 。 .
- 所有公关应当有正确的标签和有用的标题. 你们可以 在此审查可用的标签。 。 。 .
- 更新 PR 和标题的标签, 下次 手动运行发行起草器动作 重新生成释放草稿.
- 审查新版本并按"Publish"
- 如果需要检查“ 此发行的创意讨论”