Search code examples
reactjstypescriptreact-hooksreact-reduxredux-toolkit

React Component Not Re-Rendering From Redux State Change


This has been giving me a headache for awhile now. I have tried many ways but I cannot fix the problem. I had posted a few months ago and the suggestion was to convert my Redux to Redux Toolkit so I did so but the issue still persists.

My React component Component.tsx (see code below) will not re-render from Redux state changes. I am using Redux Toolkit to manage the state. Here's my code:

store.ts

import rootReducer from "./reducers/rootReducer";
import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
    reducer: rootReducer
});

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

hooks.ts

import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

rootReducer.ts

import { combineReducers } from "redux";
import assumptionsReducer from "./assumptionsSlice";
import numbersInputReducer from "./numbersInputSlice";
import paymentsInputReducer from "./paymentsInputSlice";
import numbersAnnualizationReducer from "./numbersAnnualizationSlice";

const rootReducer = combineReducers({
    assumptionsState: assumptionsReducer,
    numbersInputState: numbersInputReducer,
    paymentsInputState: paymentsInputReducer,
    numbersAnnualizationState: numbersAnnualizationReducer
});

export default rootReducer;

types.ts

    export interface numbersInputInitialStateModel {
        bankOne: number;
        bankTwo: number;
        bankThree: number;
        bankFour: number;
    };

constants.ts

const numbersInputInitialState: numbersInputInitialStateModel = {
        bankOne: 0,
        bankTwo: 0,
        bankThree: 0,
        bankFour: 0,
}

numbersInputSlice.ts

import { numbersInputInitialState } from "../../constants";
import { numbersInputInitialStateModel } from "../../types";
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../store';

type numbersInputState = numbersInputInitialStateModel;

const initialState: numbersInputState = numbersInputInitialState;

export const numbersInputSlice = createSlice({
    name: "numbersInput",
    initialState,
    reducers: {
        setNumbersInput: (state, action: PayloadAction<numbersInputInitialStateModel>) => {
            return action.payload;
          },
    }
});


export const { setNumbersInput } = numbersInputSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const numbersInputSelector = (state: RootState) => state.numbersInputState;

export default numbersInputSlice.reducer;

assumptionsSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../store';

interface assumptionsState { 
      quarter: string,
      status: string,
      people: string,
      location: string
}

const initialState: assumptionsState = { 
    quarter: "1",
    status: "RED",
    people: "0",
    location: "NONE"
}

export const assumptionsSlice = createSlice({
    name: "assumptions",
    // `createSlice` will infer the state type from the `initialState` argument
    initialState,
    reducers: {
        // Use the PayloadAction type to declare the contents of `action.payload`
        setQuarter: (state, action: PayloadAction<string>) => {
            state.quarter = action.payload
          },
        setStatus: (state, action: PayloadAction<string>) => {
            state.status = action.payload
          },
        setPeople: (state, action: PayloadAction<string>) => {
            state.people= action.payload
          },
        setLocation: (state, action: PayloadAction<string>) => {
            state.location= action.payload 
          }
    }
});

export const { setQuarter, setStatus, setPeople, setLocation } = assumptionsSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const assumptionsSelector = (state: RootState) => state.assumptionsState;

export default assumptionsSlice.reducer;

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.scss';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from "react-redux";
import { store } from "./redux/store";

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
       <App />
     </Provider>
  </React.StrictMode>,
);

Component.tsx (this component has been reduced for simplicity). This is the component that will not re-render

import { useAppSelector } from "../redux/hooks";
import { 
    numbersInputInitialStateModel,
} from "../types";


const Component = () => {

    const quarterFromStore = useAppSelector(state => state.assumptionsState.quarter);
    const numbersInputDataValues = useAppSelector(state => state.numbersInputState);
    

    return (
        <div id="numbers-output">
            <header className="box-title">SUMMARY</header>
            <table className="table">
                <thead className="table-header">
                    <tr>
                        <th scope="col">DESCRIPTION</th>
                        <th scope="col">COLUMN ONE</th>
                        <th scope="col">COLUMN TWO</th>
                    </tr>
                </thead>
                <tbody>
                         <tr className="table-row-item-regular" key={item.id}>
                            <th scope="row" className="table-description-item">TITLE</th>
                            <td>{quarterFromStore}</td>
                            <td>{numbersInputDataValues[value as keyof numbersInputInitialStateModel]}</td>
                        </tr>
               </tbody>
            </table>
        </div>
     );
}

AssumptionsBox.tsx

import { useState } from "react";
import { assumptionsElements } from "../constants";
import { useAppSelector, useAppDispatch } from "../redux/hooks";
import { setQuarter, setStatus, setPeople, setLocation} from "../redux/reducers/assumptionsSlice";

const AssumptionsBox = () => {

const dispatch = useAppDispatch();

const [ quarterState, setQuarterState ] = useState("1");
const [ statusState, setStatusState ] = useState("RED");
const [ peopleState, setPeopleState ] = useState("0");
const [ locationState, setLocationState ] = useState("NONE");

const handleSubmit = () => {
    let tempQuarter = quarterState;
    let tempStatus = statusState;
    let tempPeople = peopleState;
    let tempLocation = locationState;

    try {
        dispatch(setQuarter(tempQuarter));
        dispatch(setFilingStatus(tempStatus));
        dispatch(setPeople(tempPeople));
        dispatch(setLocation(tempLocation));
    }

    catch(err) {
        console.log(err);
    }
};

const handleChange = (elementId: number, e: { target: HTMLSelectElement }) => {
    let tempValue = e.target.value;

    // Each case goes in order from the TaxAssumptionsElements Element value from constants. If you break the order, the function will not perform properly.
    switch(elementId){
        case 0:
            setQuarterState(tempValue);
            break;
        case 1:
            setStatusState(tempValue);
            break;   
        case 2:
            setPeopleState(tempValue);
            break;
        case 3:
            setLocationState(tempValue);
            break; 
        default:
            console.log("No case was selected");    
    };
};

    return ( 
        <div>
        <div id="assumptions-box">
        <div className="container-fluid">
           <div className="box-title">ASSUMPTIONS</div>
           <div className="d-flex align-content-around flex-wrap justify-content-start">
               {assumptionsElements.map((item) =>
               <div className="p-3 assumptions-title" key={item.id}>{item.element}:&nbsp; 
                <select name={item.element} className="assumptions-select" id="assumption-item" onChange={(e) => handleChange(item.id, e)}>
                {item.array.map((value) => 
                    <option key={value.id} value={value.arrayValue}>{value.arrayValue}</option>
                )};
                </select> 
              </div>
               )}
           </div>
            <button id="assumptions-submit-button" className="submit-button" type="submit" onClick={handleSubmit}>SUBMIT</button><br/>
        </div>
       </div>
       </div>
     );
}
 
export default AssumptionsBox;

I have tried a lot. I have gone through the Redux Toolkit docs a few times. I have utilized the Redux Dev Tools and the Redux state is updating according to the Dev Tools. Among much more debugging, googling, and changing the code but all to no avail.


Solution

  • Inside Component.tsx is the way you are calling useAppSelector - it should not have a (state =>...) because that's why you are using "typed hooks" , instead useAppSelector's argument should be the numbersInputSelector selector & assumptionsSelector respectively:

    import { useAppSelector } from "../redux/hooks";
    import { 
        numbersInputInitialStateModel,
    } from "../types";
    import { numbersInputSelector } from ''; // MODIFY this line to import from numbersInputSlice
    import { assumptionsSelector } from ''; // MODIFY to correct import
    
    
    const Component = () => {
    
        // const quarterFromStore = useAppSelector(state => state.assumptionsState.quarter);
        // const numbersInputDataValues = useAppSelector(state => state.numbersInputState);
    
        // Modify above lines into:
        const quarterFromStore = useAppSelector(assumptionsSelector);
        const numbersInputDataValues = useAppSelector(numbersInputSelector);
        console.log('numbersInputSelector:', numbersInputSelector);
        
    //...
    

    Now inside the console should log the initial state, that's as from your constants.ts file:

    {
     bankOne: 0,
     bankTwo: 0,
     bankThree: 0,
     bankFour: 0,
    }
    

    Next, inside your numbersInputSlice.ts directly mutate the state because createSlice automatically use Immer.js internally to let you write simpler immutable update logic using "mutating" syntax like so:

    // ...
    export const numbersInputSlice = createSlice({
        name: "numbersInput",
        initialState,
        reducers: {
            setNumbersInput: (state, action: PayloadAction<numbersInputInitialStateModel>) => {
                state = {
                 bankOne: action.payload.bankOne,
                 bankTwo: action.payload.bankTwo,
                 bankThree: action.payload.bankThree,
                 bankFour: action.payload.bankFour
                };
              },
        }
    });
    // ...
    

    NOTE: I'd personally suggest you to not name "setAbc" the Redux actions names, as they are quite confusing with the React's setters functions, so rather rename them into "actionAbc"