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

React中使用useContext共享父子组件状态时值为空的问题

问题:React Context状态共享异常,表单值为空

我为实现注册流程的父子组件状态共享,创建了CreateCognitoUserContext及对应的Provider组件。组件树结构为SignUpView -> SignUpStepBaseView -> CreateCognitoUserView,其中CreateCognitoUserView包含id、password、email、phone四个输入框,将输入框的onChange事件绑定到Context的setter方法以更新状态。但当用户填写完表单后,点击SignUpStepBaseView中的下一步按钮时,Context中的状态值仍全部为空。请问我的Context状态共享方案是否存在错误?


ContextProvider代码

import React, {createContext, PropsWithChildren, useState} from "react";
import {ISignUpResult} from "amazon-cognito-identity-js";

export type CreateCognitoUserContextType = {
  id: string,
  setId: (id: string) => void,
  password: string,
  setPassword: (password: string) => void,
  email: string,
  setEmail: (email: string) => void,
  phone: string,
  setPhone: (phone: string) => void,
  result: ISignUpResult | null,
  setResult: (result: ISignUpResult | null) => void,
}

const CreateCognitoUserContext = createContext<CreateCognitoUserContextType>({
  id: "",
  setId: (id: string) => {},
  password: "",
  setPassword: (password: string) => {},
  email: "",
  setEmail: (email: string) => {},
  phone: "",
  setPhone: (phone: string) => {},
  result: null,
  setResult: (result: ISignUpResult | null) => {},
})
CreateCognitoUserContext.displayName = "CreateCognitoUserContext"

const CreateCognitoUserProvider = ({children}: PropsWithChildren) => {
  const [id, setId] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [email, setEmail] = useState<string>("");
  const [phone, setPhone] = useState<string>("");
  const [result, setResult] = useState<ISignUpResult | null>(null);

  return (
    <CreateCognitoUserContext.Provider
      value={{
        id: id,
        setId: (id: string) => {
          setId(id);
        },
        password: password,
        setPassword: (password: string) => {
          setPassword(password);
        },
        email: email,
        setEmail: (email: string) => {
          setEmail(email);
        },
        phone: phone,
        setPhone: (phone: string) => {
          setPhone(phone);
        },
        result: result,
        setResult: (result: ISignUpResult | null) => {
          setResult(result);
        }
      }}
    >
      {children}
    </CreateCognitoUserContext.Provider>
  );
}

export {CreateCognitoUserProvider}
export default CreateCognitoUserContext

组件树结构

SignUpView
    - SignUpStepBaseView (包含下一步和上一步按钮)
        - CreateCognitoUserView

其他相关代码

SignUpView

export default function SignUpView() {
  const {
    createCognitoUser,
  } = CreateCognitoUserController()
  const {step, steps, next, back} = SignUpController()
  const createCognitoUserContextType = useContext<CreateCognitoUserContextType>(CreateCognitoUserContext)

  const handleFirstNext = async () => {
    console.log(`id: ${createCognitoUserContextType.id}, password: ${createCognitoUserContextType.password}, email: ${createCognitoUserContextType.email}, phone: ${createCognitoUserContextType.phone}`)

    createCognitoUserContextType.setResult(
      await createCognitoUser(
        createCognitoUserContextType.id,
        createCognitoUserContextType.password,
        createCognitoUserContextType.email,
        createCognitoUserContextType.phone
      )
    )
    if (createCognitoUserContextType.result) {
      next()
    }
  }

  const getStepContent = (stepNumber: number) => {
    switch (stepNumber) {
      case 0:
        return (
          <SignUpStepBase step={stepNumber} next={handleFirstNext} children={<CreateCognitoUserView/>}/>
        );
      case 1:
        return (
          <SignUpStepBase step={stepNumber} next={next} back={back} children={<p>1111</p>}/>
        );
      case 2:
        return (
          <SignUpStepBase step={stepNumber} back={back} children={<p>2222</p>}/>
        );
      default:
        return (
          <p>??????</p>
        );
    }
  }

  return (
    <CreateCognitoUserProvider>
      <div className="Sign-Up-View" style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
        <div className="Sign-Up-Stepper" style={{height: "5%"}}>
          <Stepper activeStep={step}>
            {steps.map((label, index) => (
              <Step key={`${label} ${index}`}>
                <StepLabel>
                  <p>{label}</p>
                </StepLabel>
              </Step>
            ))}
          </Stepper>
        </div>
        <div className="Large-Divider"/>
        <div className="Sign-Up-Body-Contents" style={{height: "95%", marginLeft: "12px", marginRight: "12px"}}>
          {getStepContent(step)}
        </div>
      </div>
    </CreateCognitoUserProvider>
  );
}

CreateCognitoUserController

export default function CreateCognitoUserController() {
  const {baseEventHandle} = useHandleException<CognitoUser | any>()

  const createCognitoUserCase: CreateCognitoUserCase = new CreateCognitoUserCase(new UserRepositoryImpl(new UserApiDataSourceImpl()));

  async function createCognitoUser(id: string, password: string, email: string, phone: string): Promise<ISignUpResult | null> {
    let result: ISignUpResult | null = null

    await baseEventHandle({
      action: async () => {
        return await createCognitoUserCase.invoke(id, password, email, phone)
      },
      onSuccess: async (response: ISignUpResult) => {
        result = response
      },
      alertType: AlertType.Snackbar,
    })

    return result
  }

  return {
    createCognitoUser,
  }
}

SignUpStepBaseView

export default function SignUpStepBase({step, next, back, children}: PropsWithChildren<SignUpStepBaseProps>) {
  const {steps} = SignUpController()

  const handleNext = async (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault()
    next?.()
  }

  const handleBack = async (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault()
    back?.()
  }

  return (
    <div className="Sign-Up-Process-Body" style={{height: "95%"}}>
      <div className="Sign-Up-Process-Contents" style={{height: "95%"}}>
        {children}
      </div>
      <div className="Sign-Up-Process-Buttons" style={{float: "right"}}>
        <Button
          style={{marginRight: '10px'}}
          variant="contained"
          color="primary"
          disabled={step === 0}
          onClick={handleBack}>
          {PREV_BUTTON_TEXT}
        </Button>
        <Button variant="contained" color="primary" onClick={handleNext}>
          {step === steps.length - 1 ? (
            CONFIRM_BUTTON_TEXT
          ) : (
            NEXT_BUTTON_TEXT
          )}
        </Button>
      </div>
    </div>
  );
}

CreateCognitoUserView

export default function CreateCognitoUserView() {
  const createCognitoUserContextType = useContext<CreateCognitoUserContextType>(CreateCognitoUserContext)

  const onIdChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault()
    createCognitoUserContextType.setId(event.target.value)
  }

  const onPasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault()
    createCognitoUserContextType.setPassword(event.target.value)
  }

  const onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault()
    createCognitoUserContextType.setEmail(event.target.value)
  }

  const onPhoneChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault()
    createCognitoUserContextType.setPhone(event.target.value)
  }

  return (
    <div className="Create-Cognito-User-View" style={{flexDirection: "column"}}>
      <div className={"Create-Cognito-User-View-Id"}>
        <TextField style={{width: "360px"}} id="create-cognito-user-id-field" label="id" variant="outlined" onChange={onIdChange}/>
      </div>
      <div className="Default-Divider"/>
      <div className={"Create-Cognito-User-View-Password"}>
        <TextField style={{width: "360px"}} id="create-cognito-user-password-field" type="password" label="password" variant="outlined" onChange={onPasswordChange}/>
      </div>
      <div className="Default-Divider"/>
      <div className={"Create-Cognito-User-View-Email"}>
        <TextField style={{width: "360px"}} id="create-cognito-user-email-field" label="email" variant="outlined" onChange={onEmailChange}/>
      </div>
      <div className="Default-Divider"/>
      <div className={"Create-Cognito-User-View-Phone"}>
        <TextField style={{width: "360px"}} id="create-cognito-user-phone-field" label="phone" variant="outlined" onChange={onPhoneChange}/>
      </div>
    </div>
  );
}

问题原因

核心错误是**SignUpView在CreateCognitoUserProvider外部调用了useContext**。React的Context规则是:只有在Provider的子组件(包括深层子组件)中调用useContext,才能拿到Provider维护的状态值;如果在Provider的父组件或Provider本身的渲染范围外调用,拿到的会是Context的默认初始值(也就是你定义的空字符串、空对象)。

看SignUpView的代码,useContext调用在CreateCognitoUserProvider的返回值之外,因此拿到的始终是初始空值,哪怕CreateCognitoUserView已经正确更新了Context状态,SignUpView里的变量也不会同步。

解决方案

把需要访问Context状态的逻辑(比如handleFirstNext)移到CreateCognitoUserProvider的子组件中,确保useContext的调用在Provider的覆盖范围内。

修改后的SignUpView示例

export default function SignUpView() {
  const { createCognitoUser } = CreateCognitoUserController()
  const { step, steps, next, back } = SignUpController()

  // 将步骤0的内容封装成内部组件,确保在Provider内部调用useContext
  const Step0Content = () => {
    const ctx = useContext(CreateCognitoUserContext)
    
    const handleFirstNext = async () => {
      console.log(`id: ${ctx.id}, password: ${ctx.password}, email: ${ctx.email}, phone: ${ctx.phone}`)
      
      const signUpResult = await createCognitoUser(ctx.id, ctx.password, ctx.email, ctx.phone)
      ctx.setResult(signUpResult)
      
      if (signUpResult) {
        next()
      }
    }

    return <SignUpStepBase step={0} next={handleFirstNext} children={<CreateCognitoUserView/>}/>
  }

  const getStepContent = (stepNumber: number) => {
    switch (stepNumber) {
      case 0:
        return <Step0Content />
      case 1:
        return <SignUpStepBase step={stepNumber} next={next} back={back} children={<p>1111</p>}/>
      case 2:
        return <SignUpStepBase step={stepNumber} back={back} children={<p>2222</p>}/>
      default:
        return <p>??????</p>
    }
  }

  return (
    <CreateCognitoUserProvider>
      <div className="Sign-Up-View" style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
        <div className="Sign-Up-Stepper" style={{height: "5%"}}>
          <Stepper activeStep={step}>
            {steps.map((label, index) => (
              <Step key={`${label} ${index}`}>
                <StepLabel>
                  <p>{label}</p>
                </StepLabel>
              </Step>
            ))}
          </Stepper>
        </div>
        <div className="Large-Divider"/>
        <div className="Sign-Up-Body-Contents" style={{height: "95%", marginLeft: "12px", marginRight: "12px"}}>
          {getStepContent(step)}
        </div>
      </div>
    </CreateCognitoUserProvider>
  );
}

额外优化建议

Context的Provider中可以直接传递useState返回的setter函数,不需要额外包裹一层,简化代码:

const CreateCognitoUserProvider = ({children}: PropsWithChildren) => {
  const [id, setId] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [email, setEmail] = useState<string>("");
  const [phone, setPhone] = useState<string>("");
  const [result, setResult] = useState<ISignUpResult | null>(null);

  return (
    <CreateCognitoUserContext.Provider
      value={{
        id,
        setId,
        password,
        setPassword,
        email,
        setEmail,
        phone,
        setPhone,
        result,
        setResult
      }}
    >
      {children}
    </CreateCognitoUserContext.Provider>
  );
}

内容的提问来源于stack exchange,提问作者Jun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 12:27:51