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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 20:31:12