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

新手上路,请多包涵

新手一个,最近在学习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.dispatch
export 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.actions

export const formCode = createSelector(
  (state: RootState) => state.articles.code,
  code => code,
)

// 导出reducer属性
export default articleSlice.reducer

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

⨯ ./components/Editor.tsx
Error:
  × 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的组件判定让我头大,我的问题是:我应该如何修改这段代码以实现我想要的更新效果?
我处理类似问题的思路是不是本身就有问题,应该怎么做才对呢?
感谢。

阅读 2.5k
avatarAI BotBETA

在 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 的服务端组件和客户端组件的区别,以及它们各自的使用场景。服务端组件主要用于优化服务端渲染性能,而客户端组件则包含所有与用户交互和动态状态更新相关的逻辑。

3 个回答

不需要用状态管理,SideBarList组件数据从服务端正常获取,
表单提交成功后调用nextjs的api刷新下路由数据就行,
像nextjs官方的AI演示项目,侧边栏就是用 router.refresh 更新的

revalidatePath
router.refresh

点击提交成功后。刷新页面呗。

1. 使用 React 的 Context API

Context API 可以在组件树中共享数据,而无需显式地通过每一级组件传递 props。这适用于需要在多个组件之间共享状态的情况。

import React, { createContext, useState, useContext } from 'react';

// 创建一个 Context
const MyContext = createContext();

// 创建一个提供者组件
const MyProvider = ({ children }) => {
  const [sharedData, setSharedData] = useState('Initial Data');
  
  return (
    <MyContext.Provider value={{ sharedData, setSharedData }}>
      {children}
    </MyContext.Provider>
  );
};

// 在组件中使用 Context
const ComponentA = () => {
  const { sharedData, setSharedData } = useContext(MyContext);
  
  return (
    <div>
      <h2>Component A</h2>
      <p>Data: {sharedData}</p>
      <button onClick={() => setSharedData('Data from Component A')}>Update Data</button>
    </div>
  );
};

const ComponentB = () => {
  const { sharedData } = useContext(MyContext);
  
  return (
    <div>
      <h2>Component B</h2>
      <p>Data: {sharedData}</p>
    </div>
  );
};

// 在页面中使用
const MyApp = () => {
  return (
    <MyProvider>
      <ComponentA />
      <ComponentB />
    </MyProvider>
  );
};

export default MyApp;

2. 使用全局状态管理库

如果应用程序变得复杂,可以考虑使用状态管理库,例如 Redux、MobX 或 Recoil。这些库提供了更强大的状态管理功能,适用于大型应用程序。
首先,安装 Recoil: npm install recoil
然后,设置 Recoil 进行状态管理:

import { atom, RecoilRoot, useRecoilState } from 'recoil';

// 创建一个 Recoil 状态
const sharedDataState = atom({
  key: 'sharedDataState', // 每个 atom 需要一个唯一的 key
  default: 'Initial Data', // 默认值
});

// 在组件中使用 Recoil 状态
const ComponentA = () => {
  const [sharedData, setSharedData] = useRecoilState(sharedDataState);
  
  return (
    <div>
      <h2>Component A</h2>
      <p>Data: {sharedData}</p>
      <button onClick={() => setSharedData('Data from Component A')}>Update Data</button>
    </div>
  );
};

const ComponentB = () => {
  const [sharedData] = useRecoilState(sharedDataState);
  
  return (
    <div>
      <h2>Component B</h2>
      <p>Data: {sharedData}</p>
    </div>
  );
};

// 在页面中使用
const MyApp = () => {
  return (
    <RecoilRoot>
      <ComponentA />
      <ComponentB />
    </RecoilRoot>
  );
};

export default MyApp;

3. 使用父组件进行状态提升

将共享状态提升到共同的父组件,然后通过 props 将状态和更新函数传递给子组件。这种方法适用于状态相对简单且仅在少数组件间共享的情况。

import React, { useState } from 'react';

const ParentComponent = () => {
  const [sharedData, setSharedData] = useState('Initial Data');

  return (
    <div>
      <ComponentA sharedData={sharedData} setSharedData={setSharedData} />
      <ComponentB sharedData={sharedData} />
    </div>
  );
};

const ComponentA = ({ sharedData, setSharedData }) => {
  return (
    <div>
      <h2>Component A</h2>
      <p>Data: {sharedData}</p>
      <button onClick={() => setSharedData('Data from Component A')}>Update Data</button>
    </div>
  );
};

const ComponentB = ({ sharedData }) => {
  return (
    <div>
      <h2>Component B</h2>
      <p>Data: {sharedData}</p>
    </div>
  );
};

export default ParentComponent;
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进