1

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),
  
  })
thetada
  • 13
  • 4
  • 1
    there's not enough code here to see what's wrong - but I'd guess it's in setting up your Redux reducer. Your initial state should already be an object and have 1 as the value for the `observationStage` key, and then you won't have this problem. (Unless there is some other issue - as I said, it's hard to know for sure without seeing a lot more code.) – Robin Zigmond Dec 29 '22 at 11:13
  • Thanks, the store and redux code is working with several other components, it's just this component that's causing problems. I've added more code. – thetada Dec 29 '22 at 11:47

1 Answers1

1

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.

timotgl
  • 2,865
  • 1
  • 9
  • 19
  • thanks. Part of the difficulty was setting up a whole new section of state with the generic code (like you've used in your solution) being added to a whole series of new reactants, but actually I think I just need to completely overhaul the way the state tree is formed and get slices to append sections of generic code like yours into existing sections of state. It's possible to get a slice to file multiple, matching sets of generic code into several different parts of the existing state, right? – thetada Dec 29 '22 at 17:41
  • @thetada Don't quite understand the last question, but I can assure you that you have 100% control over the state and that you can modify anything the way you like! – timotgl Dec 30 '22 at 10:58