You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS 15 + Jotai中withImmer实现多页面进度条更新问题求助

解决NextJS 15 + Jotai + Immer多页面流程中进度条状态不更新问题

核心问题拆解

  1. 直接修改原子状态的原始对象(比如step[0].status = 'complete'),违反Jotai的不可变更新规则,导致组件无法感知状态变化
  2. setStep的更新逻辑错误:prev是数组,prev.id === 1永远不成立,等于没更新状态
  3. Immer使用方式错误:没有通过Jotai的useImmerAtom提供的更新函数修改状态,而是自己写了无效的updateStatus函数
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:27:02