React中使用useContext共享父子组件状态时值为空的问题
我为实现注册流程的父子组件状态共享,创建了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

