NextJS 15 + Jotai中withImmer实现多页面进度条更新问题求助
解决NextJS 15 + Jotai + Immer多页面流程中进度条状态不更新问题
核心问题拆解
- 直接修改原子状态的原始对象(比如
step[0].status = 'complete'),违反Jotai的不可变更新规则,导致组件无法感知状态变化 setStep的更新逻辑错误:prev是数组,prev.id === 1永远不成立,等于没更新状态- Immer使用方式错误:没有通过Jotai的
useImmerAtom提供的更新函数修改状态,而是自己写了无效的updateStatus函数 - App Router中未正确标记客户端组件,服务器组件无法响应状态变化
分步修正方案
1. 确保Signup布局是客户端组件并配置Jotai Provider
在app/signup/layout.tsx中添加客户端标记并包裹Provider,保证所有子页面共享同一状态上下文:
'use client'; import { Provider } from 'jotai'; import ProgressBar from './progress-bar'; export default function SignupLayout({ children }: { children: React.ReactNode }) { return ( <Provider> <ProgressBar /> {children} </Provider> ); }
2. 简化原子定义,直接用useImmerAtom操作
不需要单独定义derivedStepsAtomImmer,保留原始原子即可:
export const stepsAtom = atom<Step[]>([ { id: 1, name: 'Diabetes Type', href: '#', status: 'current' }, { id: 2, name: 'Gender', href: '#', status: 'upcoming' }, { id: 3, name: 'Age', href: '#', status: 'upcoming' }, { id: 4, name: 'Height', href: '#', status: 'upcoming' }, { id: 5, name: 'Weight', href: '#', status: 'upcoming' }, { id: 6, name: 'Medical Conditions', href: '#', status: 'upcoming' }, { id: 7, name: 'Physically Active', href: '#', status: 'upcoming' }, { id: 8, name: 'Exercise', href: '#', status: 'upcoming' } ]);
3. 修正Signup页面的状态更新逻辑
删除无效的updateStatus函数,用useImmerAtom的更新函数直接修改draft对象:
'use client'; import { useImmerAtom } from 'jotai/immer'; import { stepsAtom, signupFlow } from '@/path/to/your/store'; import { useRouter } from 'next/navigation'; import Button from '@/components/Button'; export default function Signup() { const [steps, setSteps] = useImmerAtom(stepsAtom); const [signUp, setSignUp] = useImmerAtom(signupFlow); const router = useRouter(); const handleClick = (event: React.FormEvent<HTMLButtonElement>, type: string) => { event.preventDefault(); // 用Immer更新signupFlow状态 setSignUp((draft) => { draft.diabetesType = type; }); // 更新步骤状态:标记第一步完成,第二步为当前 setSteps((draft) => { const firstStep = draft.find(step => step.id === 1); if (firstStep) firstStep.status = 'complete'; const secondStep = draft.find(step => step.id === 2); if (secondStep) secondStep.status = 'current'; }); router.push('/signup/step2'); }; return ( <center> <div className="flex overflow-hidden rounded-lg bg-white shadow "> <div className="px-8 py-8 sm:p-12 sm:pl-72"> <span className="font-bold">Select your diabetes type:</span> <div className="pt-6 pb-4"> <Button className="w-24 bg-indigo-500 hover:bg-indigo-700" onClick={(evt) => handleClick(evt, 'TYPE1')}>Type 1</Button> </div> <div className="pb-4"> <Button className="w-24 bg-indigo-500 hover:bg-indigo-700" onClick={(evt) => handleClick(evt, 'TYPE2')}>Type 2</Button> </div> <div className=""> <Button className="w-24 bg-indigo-500 hover:bg-indigo-700" onClick={(evt) => handleClick(evt, 'PREDIABETIC')}>Prediabetic</Button> </div> </div> </div> </center> ); }
4. 修正ProgressBar组件的细节问题
添加客户端标记,修复循环中的长度判断错误:
'use client'; import { useImmerAtom } from 'jotai/immer'; import { stepsAtom } from '@/path/to/your/store'; import { CheckIcon } from '@heroicons/react/24/solid'; export default function ProgressBar() { const [steps] = useImmerAtom(stepsAtom); return ( <nav aria-label="Progress"> <ol role="list" className="divide-y divide-gray-300 rounded-md border border-gray-300 md:flex md:divide-y-0"> {steps.map((step, stepIdx) => ( <li key={step.id} className="relative md:flex md:flex-1"> {step.status === 'complete' ? ( <a href={step.href} className="group flex w-full items-center"> <span className="flex items-center px-6 py-4 text-sm font-medium"> <span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-indigo-600 group-hover:bg-indigo-800"> <CheckIcon aria-hidden="true" className="size-6 text-white" /> </span> <span className="ml-4 text-sm font-medium text-gray-900">{step.name}</span> </span> </a> ) : step.status === 'current' ? ( <a href={step.href} aria-current="step" className="flex items-center px-6 py-4 text-sm font-medium"> <span className="flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-indigo-600"> <span className="text-indigo-600">{step.id}</span> </span> <span className="ml-4 text-sm font-medium text-indigo-600">{step.name}</span> </a> ) : ( <a href={step.href} className="group flex items-center"> <span className="flex items-center px-6 py-4 text-sm font-medium"> <span className="flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-gray-300 group-hover:border-gray-400"> <span className="text-gray-500 group-hover:text-gray-900">{step.id}</span> </span> <span className="ml-4 text-sm font-medium text-gray-500 group-hover:text-gray-900">{step.name}</span> </span> </a> )} {stepIdx !== steps.length - 1 ? ( <> <div aria-hidden="true" className="absolute right-0 top-0 hidden h-full w-5 md:block"> <svg fill="none" viewBox="0 0 22 80" preserveAspectRatio="none" className="size-full text-gray-300"> <path d="M0 -2L20 40L0 82" stroke="currentcolor" vectorEffect="non-scaling-stroke" strokeLinejoin="round" /> </svg> </div> </> ) : null} </li> ))} </ol> </nav> ); }
关键注意事项
- App Router中所有使用Jotai状态的组件必须加
'use client'标记 - 永远不要直接修改Jotai原子的原始值,必须通过
setAtom或Immer的更新函数操作 - Immer的核心是通过draft对象做可变修改,底层自动生成不可变副本,无需手动解构拷贝
内容的提问来源于stack exchange,提问作者NewbieCoder
相关产品推荐
相关产品推荐

