I’m making an educational program with React-Redux and I’m trying to make a form with items that appear and are removed sequentially. So users enter an initial observation for a reaction, click submit, the initial observation box disappears and a new button is enabled which means the user can click on a test tube and see a colour change, etc.
A useEffect function compiles an initial state for each of the reactants in question, including a value: observationStage, which starts at 1 and increases as each stage is completed. That way I can use a ternary operator so that the initial observation input disappears once it’s been completed.
{(observationStage === 1) ? <InitialObservation> : null }
Problem was the app kept crashing. I was using useSelector to obtain the value of observationStage from the state and from what I could tell, it was demanding to know the observation stage before that part of the state had been compiled.
observationStage is a key in a parentObject. If I use useSelector to get the value of parentObject and log it to the console, it logs all the keys and values, including observationStage: 1, but if I console.log parentObject.observationStage it crashes.
So I’ve done this:
const observationStage = () => { if (!parentObject){ return 1; } else { return parentObject.observationStage; }
That’s working, but now I need similar logic in a sibling component and obviously I don’t want to just repeat the same code. I could make a module just for the bit written above and import that to both the components that need it (so far), but it just feels so cumbersome.
I’d really appreciate if anyone could see a glaring problem with my approach and set me straight. Thanks in advance.
Here’s a more detailed set of code snippets
//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;
//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])
//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;
//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), })
Advertisement
Answer
I was using useSelector to obtain the value of observationStage from the state and from what I could tell, it was demanding to know the observation stage before that part of the state had been compiled.
So the difficulty here is that your redux state contains complex (nested) objects with unknown keys. The react components expect the individual keys of these metal/reactant objects to be present to work properly.
The code you have in your useEffect
is already part of the solution:
// An empty or "initial" object that doesn't contain anything meaningful but mimics the expected structure sufficiently. const getEmptyReactant = () => ({ observationStage: 1, initial: { input: '', logged: '' }, final: { input: '', logged: '' } }); const selectReactant = (metal) => state.reactantsToObserve[metal] ? state.reactantsToObserve[metal] : getEmptyReactant();
This should be enough of a basis to refactor your code in a way that eliminates the problem. The gist here is that these kind of state shape issues need to be solved in the redux layer. The react components should do as little as possible and leave the work up to selector functions.