当前位置: 首页 > 知识库问答 >
问题:

next.js - 在nextjs中,如何才能进行两个共享数据的组件的通信?

连昊天
2024-06-03

新手一个,最近在学习nextjs14,基于App Router做一个小的笔记本demo.
大致想要的效果如下:
image.png

  • 简介:
    这里面有两个组件,一个是SideBarNoteList,控制笔记列表,一个是Editor,书写内容,提交内容。Editor的父组件是动态路的page.tsx。笔记的数据源只是一个静态json数据,想简单的模拟一下。结构大致如此:image.png
    我的需求也很简单,就是在提交笔记表单后,action会把数据添加到静态数据里,然后我希望左边的SideBarList能把新的数据刷出来
    按照我之前的react经验,我使用了redux来管理状态,创建了一个store,我的想法是使用useState/useEffect/useSelector/dispatch,在表单提交后讲数据存到store中,SideBarList监听store中的表单状态,有表单提交时刷新自己,重新获取新的数据并渲染。
  • 代码片段

    • SideBarList的代码:
export default function SideBarNoteList() {  const getFormCode = useSelector(formCode)  const [articleData, setArticleData] = useState<Article[]>(listAllArticles())  if (articleData.length === 0)    return <div className="notes-empty">No notes created yet!</div>  useEffect(() => {    // 监听表单状态变化    if (getFormCode === EditorFormStateCode.Success) {      // 表单提交成功后,重新获取数据      console.log('表单提交成功后,重新获取数据')      setArticleData(listAllArticles())    }  }, [getFormCode])
  • 动态路由下的page.tsx包含了Editor组件:
export default function Page({ params }: { params: { id: string } }) {  const noteId = params.id  const note = getArticleById(noteId)  if (!note) {    return (      <div className="note--empty-state">        <span className="note-text--empty-state">          Click a note on the left to view something! ��        </span>      </div>    )  }  return (    <Editor      noteId={noteId}      initialTitle={note.title}      initialBody={note.content}    />  )}

Editor组件监听了store中的formstate:

export enum EditorFormStateCode {  Success = 'success',  Error = 'error',}export default function Editor({  noteId,  initialTitle,  initialBody,}: {  noteId: string | null  initialTitle: string  initialBody: string}) {  const [title, setTitle] = useState(initialTitle)  const [body, setBody] = useState(initialBody)  const [saveState, saveFormAction] = useFormState<EditorFormState, FormData>(    saveNote,    initialState,  )  const [deleteState, deleteFormAction] = useFormState<EditorFormState, FormData>(    deleteNote,    initialState,  )  useEffect(() => {    if (saveState?.code === EditorFormStateCode.Success) {      // 派发表单提交事件      store.dispatch(articleSliceActions.formSubmit(saveState))    }    if (deleteState?.code === EditorFormStateCode.Success) {      // 派发表单提交事件      store.dispatch(articleSliceActions.formSubmit(deleteState))    }  }, [saveState, deleteState])

我的根Layout.tsx:

export default function RootLayout({  children,}: Readonly<{  children: React.ReactNode}>) {  return (    <html lang="en">      <body>        <Provider store={store}>          <div className="container">            <div className="main">              <Sidebar />              <section className="col note-viewer">{children}</section>            </div>          </div>        </Provider>      </body>    </html>  )}
* 我的store.ts:
export const store = configureStore({  reducer: rootReducer,  devTools: process.env.NODE_ENV !== 'production',  // middleware: getDefaultMiddleware => getDefaultMiddleware({  //   serializableCheck: false, // 禁用序列化检查  // }).concat(logger),})export type AppDispatch = typeof store.dispatchexport type RootState = ReturnType<typeof rootReducer>export type AppThunk<ReturnType = void> = ThunkAction<  ReturnType,  RootState,  unknown,  Action<string>>

一个很简单的slice:

export interface EditorFormState {  message: string  code: string  errors?: ZodIssue[]}export const initialState: EditorFormState = {  message: '',  code: EditorFormStateCode.Error,  errors: [],}const articleSlice = createSlice({  name: 'articles',  initialState,  reducers: {    formSubmit(state, { payload }: PayloadAction<EditorFormState>) {      state.message = payload.message      state.code = payload.code      state.errors = payload.errors    },  },})export const articleSliceActions = articleSlice.actionsexport const formCode = createSelector(  (state: RootState) => state.articles.code,  code => code,)// 导出reducer属性export default articleSlice.reducer

现在我的代码启动后报如下错误:

⨯ ./components/Editor.tsxError:  × You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.  │ Learn more: https://nextjs.org/docs/getting-started/react-essentials

看了问题说明,似乎是说被next判定为Server component 的不能使用useSate/useEffect等。
其中我的Editor是被判定成了Server component
于是我尝试在Editor开头加上'use client',试图让Editor被判定为一个Client Component,加上之后错误变成了:

⨯ Error: Cannot access Error.Symbol(immer-state) on the server. You cannot dot into a client module from a server component. You can only pass the imported name through.

nextjs的组件判定让我头大,我的问题是:我应该如何修改这段代码以实现我想要的更新效果?
我处理类似问题的思路是不是本身就有问题,应该怎么做才对呢?
感谢。

共有1个答案

曾皓
2024-06-03

在 Next.js 中,如果你想要在组件中使用 React 的状态钩子(如 useStateuseEffect),那么这个组件必须是一个客户端组件(Client Component)。Next.js 允许你通过 'use client' 指令来标记一个组件为客户端组件。但是,你遇到的 Error.Symbol(immer-state) 错误可能是因为你在服务端组件中访问了只在客户端可用的状态或状态管理库(如 Redux 或 Immer)。

为了解决你的问题,你需要确保你的状态管理和组件逻辑只在客户端运行。以下是一些步骤和修改建议:

  1. 标记 Editor 为客户端组件
    在 Editor 组件的顶部添加 'use client' 指令。这将告诉 Next.js 这个组件只在客户端渲染,并且可以使用客户端特有的 React Hooks。

    // Editor.tsx'use client';import React, { useState, useEffect } from 'react';// ... 其他导入和组件代码
  2. 确保状态更新仅在客户端执行
    由于你使用的是 Redux 来管理状态,确保你的 Redux store 和相关的选择器(selectors)仅在客户端初始化。Next.js 允许你在客户端初始化 Redux store,然后在客户端组件中使用它。

    // _app.js 或根组件'use client';import { Provider } from 'react-redux';import { store } from '../store';import { wrapper } from 'next-redux-wrapper';function MyApp({ Component, pageProps }) {  return (    <Provider store={store}>      <Component {...pageProps} />    </Provider>  );}export default wrapper.withRedux(MyApp);

注意:next-redux-wrapper 是一个库,可以帮助你在 Next.js 应用中集成 Redux,并在服务端和客户端之间同步状态。但是,如果你只是简单地想要在客户端使用 Redux,并且不关心服务端渲染时的状态,那么你可以不使用 next-redux-wrapper,直接在客户端初始化 Redux store。

  1. 避免在 Server Components 中使用状态 Hooks
    确保你没有在 Server Components 中使用 useStateuseEffect。Server Components 是为了优化服务端渲染而设计的,它们不应该包含任何客户端特有的逻辑或状态。
  2. 调整数据获取逻辑
    如果你的数据源是一个静态 JSON 文件,并且你想要在客户端添加新数据后刷新列表,你应该在客户端处理这个逻辑。你可以使用 Redux action 来更新 store 中的数据,并在 SideBarList 组件中监听这些数据的变化。

    确保 SideBarList 组件也是一个客户端组件,并使用 useSelector 从 Redux store 中选择数据。这样,当 store 中的数据更新时,SideBarList 组件会重新渲染以显示最新的数据。

  3. 处理 Next.js 的数据获取
    如果你需要从服务端获取数据并在客户端显示,可以使用 Next.js 的数据获取方法,如 getServerSidePropsgetInitialProps。但是,这些方法返回的数据将直接作为 props 传递给页面组件,并且不会在客户端更新。对于需要在客户端更新的数据,你应该使用客户端状态管理(如 Redux)。

最后,确保你理解 Next.js 的服务端组件和客户端组件的区别,以及它们各自的使用场景。服务端组件主要用于优化服务端渲染性能,而客户端组件则包含所有与用户交互和动态状态更新相关的逻辑。

 类似资料:
  • 问题内容: 我的GUI中有多个组合框,所有组合框都需要数据。此数据将随机更改,因此需要一种快速的方法来保持所有值同步。我遇到了DefaultComboBoxModel,它实际上非常合适。唯一的事情是,我需要组合框彼此独立- 意思是:如果我在一个上选择一个值,则所有其他框都不应更改。我做了一些研究,并阅读了标准的Java教程,但没有一个告诉我如何使用DefaultComboBoxModel来实现这一

  • 问题内容: 我需要在两个(或更多个)组合框之间共享数据,但是我想独立选择元素。例如,如果我在第一个comboBox中选择Object1,则我的第二个ComboBox也选择Object1,因为它们具有相同的模型(DefaultComboBoxModel,并且此模型还管理所选的对象)。但是我不想要这种行为。我想在我的comboBoxes中独立选择对象。当我在第一个comboBox中选择对象时,我的第二

  • 问题内容: 我正在将AngularJS与c#mvc一起使用。我有一个主页,用户可以在其中输入一些数据,并将其传递给第二个模块,在该模块中,我将使用这些数据进行处理和决策。我必须使用第二个模块中第一个模块中输入或更新的数据。有人可以帮我实现这个目标吗? 问题答案: 希望以下实现可以帮助您有所了解。

  • 在Angular 1.x.x中,您只需请求相同的服务,并以相同的实例结束,这使得在服务中共享数据成为可能。 现在在Angular 2中,我有了一个引用我的服务的组件。我可以读取和修改服务中的数据,这很好。当我试图在另一个组件中注入相同的服务时,似乎得到了一个新的实例。 我做错了什么?是模式本身有问题(使用服务共享数据)还是我需要将服务标记为单例(在应用程序的一个实例中)或其他? 我在 btw上 我

  • 问题内容: 我有两个SwingWorker类:和 我将执行两个线程。当计数线程的行数结束时,它将结果传递给File Division线程。 我不知道如何将结果传递到启动线程。 问题答案:

  • 问题内容: 我正在开发使用Meteor和React作为视图引擎的应用程序 考虑下图: 从另一个示例反应隐藏组件 当触发C4按钮单击事件时,我需要更改C2组件状态。由于他们没有直接关系,因此我无法直接从C4进入C2状态。 另一个示例是从Component提交表单并获取在另一个Component中声明的数据(输入字段的值)。 我知道可以通过一些技巧来解决此问题(例如,流星会话,通过每个组件传递数据,基