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),
})