Next.js 13 AppDir集成React-Redux时JSON解析错误求助
问题描述
在启用appDir的Next.js 13项目中使用React-Redux做状态管理,启动应用时触发如下错误:
caught SyntaxError: "undefined" is not valid JSON
at JSON.parse ()
at parseModel (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:33:15)
移除React-Redux的Provider组件后应用可正常运行,重新添加则错误复现。未在代码中直接使用JSON.parse,仅在layout.tsx中用ApolloProvider和Provider包裹子组件。已尝试更换Node.js版本、删除node_modules后重装依赖,问题仍未解决。
相关代码文件
layout.tsx
"use client" import { Poppins } from "next/font/google"; import SiteHeader from "./(client-components)/(Header)/SiteHeader"; import ClientCommons from "./ClientCommons"; import "./globals.css"; import "@/fonts/line-awesome-1.3.0/css/line-awesome.css"; import "@/styles/index.scss"; import "rc-slider/assets/index.css"; import Footer from "@/components/Footer"; import FooterNav from "@/components/FooterNav"; import { ApolloProvider } from "@apollo/react-hooks"; import { Provider } from "react-redux"; import { store } from "../lib/store"; import client from "../lib/graphql"; const poppins = Poppins({ subsets: ["latin"], display: "optional", weight: ["100", "300", "400", "500", "600", "700", "800"], }); export default function RootLayout({ children, params, }: { children: React.ReactNode; params: any; }) { return ( <html lang="en" className={poppins.className}> <body className="bg-white text-base dark:bg-neutral-900 text-neutral-900 dark:text-neutral-200"> <ApolloProvider client={client}> <Provider store={store}> <ClientCommons /> <SiteHeader /> {children} <FooterNav /> <Footer /> </Provider> </ApolloProvider> </body> </html> ); }
propertySlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { fetchProperties, fetchProperty } from "./propertiesApi"; const initialState = { data: [], propertyData: null, loading: true, error: null, searchQuery: { location: "", priceMin: null, priceMax: null, propertyTypes: [], }, property: {}, selectedListingId: null, }; const propertiesSlice = createSlice({ name: "properties", initialState, reducers: { setSelectedListing(state, action) { state.selectedListingId = action.payload; }, setProperties(state, action) { state.items = action.payload; }, setSearchQuery(state, action) { state.searchQuery = action.payload; }, setProperty(state, action) { state.property = action.payload; }, setLoading(state, action) { state.loading = action.payload; }, setError(state, action) { state.error = action.payload; }, }, extraReducers: (builder) => { builder .addCase(fetchProperties.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchProperties.fulfilled, (state, { payload }) => { state.loading = false; state.data = payload; }) .addCase(fetchProperties.rejected, (state, { payload }) => { state.loading = false; state.error = payload; }) .addCase(fetchProperty.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchProperty.fulfilled, (state, { payload }) => { state.loading = false; state.error = null; state.property = payload; }) .addCase(fetchProperty.rejected, (state, { payload }) => { state.loading = false; state.error = payload; }); }, }); export const { setProperty, setLoading, setError, setProperties, setSearchQuery, setSelectedListing, } = propertiesSlice.actions; export default propertiesSlice.reducer;
propertiesApi.ts
import { createAsyncThunk } from "@reduxjs/toolkit"; import { store } from "@/lib/store"; import client from "../../lib/graphql"; import { GET_AllProperties, Get_Property } from "../../graphql/queries"; import { useDispatch } from "react-redux"; export const fetchProperties = createAsyncThunk( "properties/fetchProperties", async () => { const { data } = await client.query({ query: GET_AllProperties, }); return data.allProperties; } ); export const fetchProperty = createAsyncThunk( "property/fetchProperty", async (id) => { const variables = { id }; const { data } = await client.query({ query: Get_Property, variables, }); return data.property; } ); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>; export const useAppDispatch = () => useDispatch<AppDispatch>();
store.js
import { configureStore } from "@reduxjs/toolkit"; import propertiesSlice from '../features/properties/propertiesSlice' import userSlice from '../features/user/userSlice' export const store = configureStore({ reducer: { properties: propertiesSlice, user: userSlice, }, });
Package.json
{ "name": "chisfis-nextjs", "version": "0.2.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@apollo/react-hooks": "^4.0.0", "@headlessui/react": "^1.7.12", "@heroicons/react": "^2.0.16", "@mapbox/mapbox-gl-geocoder": "^5.0.1", "@mapbox/mapbox-sdk": "^0.15.0", "@netlify/plugin-nextjs": "^4.33.0", "@reduxjs/toolkit": "^1.9.3", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.9", "@types/google-map-react": "^2.1.7", "@types/node": "18.14.2", "@types/react": "18.0.28", "@types/react-datepicker": "^4.10.0", "@types/react-dom": "18.0.11", "client-only": "^0.0.1", "eslint": "8.35.0", "eslint-config-next": "^13.2.3", "framer-motion": "^10.0.1", "geocoding": "^1.0.0", "google-map-react": "^2.2.0", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", "latest": "^0.2.0", "lodash": "^4.17.21", "mapbox-gl": "^2.13.0", "next": "^13.2.4-canary.9", "next-auth": "^4.20.1", "rc-slider": "^10.1.1", "react": "^18.2.0", "react-datepicker": "^4.10.0", "react-dom": "^18.2.0", "react-hooks-global-state": "^2.1.0", "react-icons": "^4.8.0", "react-map-gl": "^7.0.21", "react-redux": "^8.0.0-beta.1", "react-swipeable": "^7.0.0", "react-use": "^17.4.0", "react-use-keypress": "^1.3.1", "react-whatsapp": "^0.3.0", "redux": "^4.2.1", "sass": "^1.58.3", "server-only": "^0.0.1", "sharp": "^0.32.0", "typescript": "4.9.5" }, "devDependencies": { "@types/mapbox__mapbox-gl-geocoder": "^4.7.3", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", "tailwindcss": "^3.2.7", "webpack": "^5.78.0" } }
解决方案
1. 升级React-Redux到稳定版
当前使用的react-redux是测试版8.0.0-beta.1,和Next.js 13 App Router存在兼容性风险。执行以下命令升级:
npm install react-redux@latest
2. 修复Redux状态序列化问题
Next.js App Router在服务器/客户端组件交互时会序列化上下文数据,若Redux状态包含不可序列化的值(如函数、循环引用、Apollo Client缓存对象),会触发JSON解析错误:
- 检查
userSlice的初始状态和reducer逻辑,确保所有状态值均为可序列化类型(字符串、数字、数组、普通对象) - 确保AsyncThunk返回的payload是纯JSON数据,避免传递Apollo Client的原始响应对象
3. 调整Provider的包裹方式
RootLayout作为根布局组件,直接添加"use client"可能导致上下文传递异常。将Provider移到单独的客户端组件中:
新建Providers.tsx:
"use client" import { ApolloProvider } from "@apollo/react-hooks"; import { Provider } from "react-redux"; import { store } from "../lib/store"; import client from "../lib/graphql"; export default function Providers({ children }: { children: React.ReactNode }) { return ( <ApolloProvider client={client}> <Provider store={store}> {children} </Provider> </ApolloProvider> ) }
修改layout.tsx,移除"use client"并引入Providers组件:
import { Poppins } from "next/font/google"; import SiteHeader from "./(client-components)/(Header)/SiteHeader"; import ClientCommons from "./ClientCommons"; import "./globals.css"; import "@/fonts/line-awesome-1.3.0/css/line-awesome.css"; import "@/styles/index.scss"; import "rc-slider/assets/index.css"; import Footer from "@/components/Footer"; import FooterNav from "@/components/FooterNav"; import Providers from "./Providers"; const poppins = Poppins({ subsets: ["latin"], display: "optional", weight: ["100", "300", "400", "500", "600", "700", "800"], }); export default function RootLayout({ children, params, }: { children: React.ReactNode; params: any; }) { return ( <html lang="en" className={poppins.className}> <body className="bg-white text-base dark:bg-neutral-900 text-neutral-900 dark:text-neutral-200"> <Providers> <ClientCommons /> <SiteHeader /> {children} <FooterNav /> <Footer /> </Providers> </body> </html> ); }
4. 完善AsyncThunk错误处理
当请求失败时,确保返回可序列化的错误信息,避免undefined进入Redux状态:
export const fetchProperties = createAsyncThunk( "properties/fetchProperties", async (_, { rejectWithValue }) => { try { const { data } = await client.query({ query: GET_AllProperties, }); return data.allProperties; } catch (err) { return rejectWithValue(err.message || "获取房源列表失败"); } } ); export const fetchProperty = createAsyncThunk( "property/fetchProperty", async (id, { rejectWithValue }) => { try { const variables = { id }; const { data } = await client.query({ query: Get_Property, variables, }); return data.property; } catch (err) { return rejectWithValue(err.message || "获取房源详情失败"); } } );
内容的提问来源于stack exchange,提问作者Favour Samson

