React-Redux中useSelector状态未就绪时如何优雅设置默认值?
React-Redux分步表单状态复用优化方案
问题背景
我在开发一个基于React-Redux的教育类应用,需要实现分步显示/隐藏的表单功能:用户提交初始观察值后,初始输入框隐藏,后续操作按钮启用,用户还能点击试管查看颜色变化。
原本的实现思路是:通过useEffect为目标反应物生成初始状态,其中包含observationStage字段(初始值1,每完成一个阶段就递增),用三元运算符控制输入框的显示:
{(observationStage === 1) ? <InitialObservation> : null }
但应用频繁崩溃,排查后发现:用useSelector获取状态时,observationStage对应的状态还没初始化完成——打印整个父对象能看到完整的observationStage:1键值对,但直接访问parentObject.observationStage就会崩溃。
我写了下面的逻辑暂时解决了问题:
const observationStage = () => { if (!parentObject){ return 1; } else { return parentObject.observationStage; } }
现在这个逻辑能正常工作,但需要在兄弟组件里复用,不想重复写代码,也不想单独创建模块,求更优的实现方案。
优化方案
1. 自定义Hook(推荐)
自定义Hook是React中复用组件逻辑的标准方式,无需额外模块,直接封装状态判断逻辑即可:
// 可放在ObservationForm组件文件内,或单独的hooks文件中 import { useSelector } from 'react-redux'; const useObservationStage = (metal) => { const metalObservations = useSelector(state => state.observationFormSlice.reactantsToObserve[metal.metal]); // 用可选链+空值合并简化判断,安全获取状态 return metalObservations?.observationStage ?? 1; };
在组件中使用
替换原来的observationStage函数,直接用变量而非函数调用:
const ObservationForm = (props) => { const dispatch = useDispatch(); const metal = props.props.metal; // 使用自定义Hook获取阶段值 const observationStage = useObservationStage(metal); const metalObservations = useSelector(state => state.observationFormSlice.reactantsToObserve[metal.metal]); // ... 其他输入处理逻辑不变 const submitObservation = (event) => { event.preventDefault(); if (observationStage === 1){ dispatch(logInitialObservation({ metal: metal.metal, observation: metalObservations.initial.input, observationStage: observationStage + 1 })); return; } else if (observationStage === 2){ dispatch(logFinalObservation({ metal: metal.metal, observation: metalObservations.final.input, observationStage: observationStage + 1 })); return; } } // 渲染部分直接使用变量 return ( <div className="form-check translate-middle-x"> <form> {(observationStage === 1) ? <div> <label>Initial observation</label> <input type="text" onChange={initialObservationToState} id={`flexCheck${props.props.metal.id}-initial`}/> </div> : null } {(observationStage === 2) ? <div> <label>Final observation</label> <input type="text" onChange={finalObservationToState} id={`flexCheck${props.props.metal.id}-final`}/> </div> : null } {/* 提交按钮部分不变 */} <ul className="list-group list-group-horizontal mt-3 fs-5 d-flex justify-content-center"> <div className="excess-or-reset-button-container d-flex justify-content-center"> <button className="excess-button list-group-item w-100 rounded" type="submit" id="submitObservation" onClick={submitObservation} >Submit observation</button> </div> </ul> </form> <p>Hello!</p> </div> ) }
2. 从根源避免状态未初始化问题
还可以在Redux Slice中添加默认值判断,确保访问不存在的金属状态时不会返回undefined,彻底消除崩溃隐患:
// 修改observationFormSlice中的reducer inputInitialObservation: (state, action) => { const { metal, observation } = action.payload; // 若该金属未初始化状态,先创建默认结构 if (!state.reactantsToObserve[metal]) { state.reactantsToObserve[metal] = { observationStage: 1, initial: { input: '', logged: '' }, final: { input: '', logged: '' } }; } state.reactantsToObserve[metal].initial.input = observation; }, // 同理修改inputFinalObservation、logInitialObservation等reducer inputFinalObservation: (state, action) => { const { metal, observation } = action.payload; if (!state.reactantsToObserve[metal]) { state.reactantsToObserve[metal] = { observationStage: 1, initial: { input: '', logged: '' }, final: { input: '', logged: '' } }; } state.reactantsToObserve[metal].final.input = observation; },
原始代码片段
ObservationForm组件
//this is the observationForm component import { useSelector, useDispatch } from 'react-redux'; import { inputInitialObservation, inputFinalObservation, logInitialObservation, logFinalObservation } from './observationFormSlice'; import '../../app/App.css'; const ObservationForm = (props) => { const dispatch = useDispatch(); const metal = props.props.metal; const metalObservations = useSelector(state => state.observationFormSlice.reactantsToObserve[metal.metal]); const observationStage = () => { if (!metalObservations){ return 1; } else { return metalObservations.observationStage; } } const initialObservationToState = (event) => { dispatch(inputInitialObservation({metal: metal.metal, observation: event.target.value})); } const finalObservationToState = (event) => { dispatch(inputFinalObservation({metal: metal.metal, observation: event.target.value})); } const submitObservation = (event) => { event.preventDefault(); if (observationStage() === 1){ dispatch(logInitialObservation({metal: metal.metal, observation: metalObservations.initial.input, observationStage: observationStage() + 1})); return; } else if (observationStage() === 2){ console.log(observationStage() + 1); dispatch(logFinalObservation({metal: metal.metal, observation: metalObservations.final.input, observationStage: observationStage() + 1})); return; } } return ( <div className="form-check translate-middle-x"> <form> {/*Submit initial observation */} {(observationStage() === 1) ? <div> <label> Initial observation </label> <input type="text" onChange={initialObservationToState} id={`flexCheck${props.props.metal.id}-initial`}/> </div> : null } {/*Submit second observation */} {(observationStage() === 2) ? <div> <label> Final observation </label> <input type="text" onChange={finalObservationToState} id={`flexCheck${props.props.metal.id}-final`}/> </div> : null } {/*submit button */} <ul className="list-group list-group-horizontal mt-3 fs-5 d-flex justify-content-center"> <div className="excess-or-reset-button-container d-flex justify-content-center"> <button className="excess-button list-group-item w-100 rounded" type="submit" id="submitObservation" onClick={submitObservation} >Submit observation</button> </div> </ul> </form> <p>Hello!</p> </div> ) } export default ObservationForm;
父组件中的useEffect
//effect hook in parent component hook assigns a set of reactants for which to file observations useEffect(() => { let objectOfReactantsToObserve = {} unreactedMetals.map((entry) => { objectOfReactantsToObserve = {...objectOfReactantsToObserve, [entry.metal]: {observationStage: 1, initial: {input: '', logged: ''}, final: {input: '', logged: ''}}} }) dispatch(selectReactantsToObserve(objectOfReactantsToObserve)); }, [unreactedMetals, reactant])
ObservationForm的Redux Slice
//this is the slice for observationForm code import { createSlice } from '@reduxjs/toolkit'; export const observationFormSlice = createSlice({ name: "observationForm", initialState: { reactantsToObserve: {} }, reducers: { selectReactantsToObserve: (state, action) => { state.reactantsToObserve = action.payload; }, inputInitialObservation: (state, action) => { state.reactantsToObserve[action.payload.metal].initial.input = action.payload.observation; }, inputFinalObservation: (state, action) => { state.reactantsToObserve[action.payload.metal].final.input = action.payload.observation; }, logInitialObservation: (state, action) => { state.reactantsToObserve[action.payload.metal].initial.logged = action.payload.observation; state.reactantsToObserve[action.payload.metal].observationStage = action.payload.observationStage; }, logFinalObservation: (state, action) => { state.reactantsToObserve[action.payload.metal].final.logged = action.payload.observation; state.reactantsToObserve[action.payload.metal].observationStage = action.payload.observationStage; }, reset: (state) => { state.reactantsToObserve = {}; } }, }); export const { selectReactantsToObserve, inputInitialObservation, inputFinalObservation, logInitialObservation, logFinalObservation, reset } = observationFormSlice.actions; export default observationFormSlice.reducer;
Redux Store配置
//this is the code in the store import { configureStore } from '@reduxjs/toolkit'; import examBoardReducer from '../features/examBoards/examBoardsSlice.js'; import menuReducer from '../features/menu/menuSlice.js'; import multipleChoiceQuestionReducer from '../features/textBoxCreator/textBoxElements/multipleChoiceQuestions/multipleChoiceQuestionSlice'; import textBoxCreatorReducer from '../features/textBoxCreator/textBoxCreatorSlice'; import rowOfTubesReducer from '../features/rowOfTestTubes/rowOfTestTubesSlice'; import observationFormReducer from '../features/observations/observationFormSlice'; import { reHydrateStore, localStorageMiddleware } from '../features/examBoards/examBoardMiddleware'; export default configureStore({ reducer: { examBoard: examBoardReducer, menu: menuReducer, rowOfTubes: rowOfTubesReducer, textBoxCreator: textBoxCreatorReducer, multipleChoiceQuestion: multipleChoiceQuestionReducer, observationFormSlice: observationFormReducer }, preloadedState: reHydrateStore(), middleware: getDefaultMiddleware => getDefaultMiddleware().concat(localStorageMiddleware), })
内容的提问来源于stack exchange,提问作者thetada
相关产品推荐
相关产品推荐

