Search code examples
reactjstypescriptreduxreact-hooksreact-redux

React Component Will Not Re-Render After Redux Updates


I am new to React and Redux and I have been working on this issue for a few days now.

The problem is my react component will not re-render once Redux updates. I have added Redux Dev Tools and the state updates correctly so at this point I have no idea why my React component will not re-render. I have tried reading the docs and searching Stackoverflow but still I cannot figure out what I am doing wrong. The component (ComponentOne) in question renders initially with the default state from redux but when I change the Redux state through a different component (ComponentTwo), ComponentOne will not re-render. Redux updates in Dev tools. I am updating the redux in ComponentOne and expecting it to change in ComponentTwo. The only time ComponentTwo will re-render is if the props of ComponentTwo change.

I am using Typescript, React, and Redux.

Please disregard that I am using Redux and React state in component #1. I plan on changing this eventually. I have simplified some of the components because there is a lot of code. It should not impact the question.

Structure

|__src
|  |__components
|  |  |__ComponentOne.tsx -redux dispatched, useDispatch
|  |  |__ComponentTwo.tsx -component that will not re-render, uses useSelector
|  |__functions
|  |  |__functions.ts
|  |__redux
|  |  |__store.ts
|  |  |__actionsMaster.ts
|  |  |__reducers
|  |  |  |__quarterReducer.ts
|  |  |  |__filingStatusReducer.ts
|  |  |  |__rootReducer.ts

ComponentOne.tsx

import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { changeQuarter, changeFilingStatus } from "../redux/action-creators/actionsMaster";
import { constantsOne } from "../constants";
import { RootState } from "../redux/store";

const ComponentOne= () => {

const quarter = useSelector<RootState, string>(state => state.quarter);
const filingStatus = useSelector<RootState, string>(state => state.filingStatus);
const dispatch = useDispatch();

const [ quarterState, setQuarterState ] = useState("1");
const [ filingStatusState, setFilingStatusState ] = useState("SINGLE");

const handleSubmit = () => {
  let tempQuarter = quarterState;
  let tempFilingStatus = filingStatusState;

try {
  dispatch(changeQuarter(tempQuarter));
  dispatch(changeFilingStatus(tempFilingStatus));
  setQuarterState(tempQuarter);
  setFilingStatusState(tempFilingStatus);
     }

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

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

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

return ( 
         <div>
         <div id="component1-box">
         <div className="container-fluid">
            <div className="box-title">COMPONENT 1 TITLE</div>
            <div className="d-flex align-content-around flex-wrap justify-content-start">
               {constantsOne.map((item) =>
                <div className="p-3 component1-title" key={item.id}>{item.element}:&nbsp; 
                 <select name={item.element} className="component1-select" id="component1-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="component1-submit-button" className="submit-button" type="submit" onClick={handleSubmit}>SUBMIT</button><br/>
         </div>
        </div>
        </div>
      );
 }

export default ComponentOne;

ComponentTwo.tsx

import functionOne from "../functions/functionOne";
 import functionTwo from "../functions/functionTwo";
 import functionThree "../functions/functionThree";
 import { useSelector } from "react-redux";
 import { 
  modelOneType,
  modelTwoType,
  modelThreeType
 from "../types";
 import { 
  constantsTwo, 
  constantsThree }
 from "../constants";
 import { RootState } from "../redux/store";
 
 interface componentTwoProps {
  props1: modelOneType,
  props2: modelTwoType,
  props3: modelThreeType
 }
 
 const ComponentTwo= ({ 
  props1,
  props2, 
  props3
 }: componentTwoProps 
 ) => {
 
 const quarterFromStore = useSelector<RootState, string>(state => state.quarter);
 const filingStatusFromStore = useSelector<RootState, string>(state => state.filingStatus);
 
 const valueOne: number = functionOne(quarterFromStore);
 const ValueTwo: props1Type = functionTwo(props1, props2, valueOne);
 const valueThree: number = functionThree(valueTwo, constantsTwo);
 
 // There are many more consts that have new data based off of other consts. I excluded because I    // do not think it impacts the problem at hand. There are really up to like valueThirty.
 // the div below has also been simplified but should not impact issue at hand

 return (
         <div id="numbers-output-with-tax">
             <header className="box-title">COMPONENT TWO TITLE</header>
           <table className="table">
                 <thead className="table-header">
                    <tr>
                       <th scope="col">DESCRIPTION</th>
                        <th scope="col">COLUMN ONE</th>
                    </tr>
                 </thead>
                <tbody>
                        {constantsTwo.map((item) => 
                       <tr className="table-row-item-regular" key={item.id}>
                            <th scope="row" className="table-description-item">{item.element}</th>
                          <td>{valueTwo[item.hardValue as keyof modelOneType]}</td>
                         </tr>
                       )}

                         <tr className="table-total-row">
                            <th scope="row">TOTAL</th>
                            <td>{valueThree}</td>
                        </tr>
 
                       {constantsThree.map((item) => 
                       <tr className="table-row-item-regular" key={item.id}>
                            <th scope="row" className="table-description-item">{item.element}</th>
                            <td>{valueTwo[item.hardValue as keyof modelOneType]}</td>
                        </tr>
                        )}
                 </tbody>
            </table>
         </div>
      );
 }

 export default ComponentTwo;

App.tsx

import Header from './components/Header'
import UserTitleBox from './components/UserTitleBox';
import ComponentOne from './components/ComponentOne';
import InputComponent from './components/InputComponent';
import Notes from './components/Notes';
import Contact from './components/Contact';
import Footer from './components/Footer'


function App() {
  return (
    <div className="App">
         <Header/>
         <UserTitleBox />
         <ComponentOne/>
         <InputComponent />

        // ComponentTwo is a child of InputComponent

        <Notes />
        <Contact />         
        <Footer />     
    </div>
  );
}

export default App;

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>;

actionsMaster.ts

import { 
    modelOneType, 
    modelTwoType,
    modelThreeType
} from "../../types";
import { Dispatch } from 'redux';
import { 
    CHANGEQUARTER, 
    CHANGEFILINGSTATUS,
    CHANGENUMBERSINPUT,
    CHANGENUMBERSANNUALIZATION,
    CHANGEPAYMENTSINPUT
 } from '../constants';

export const changeQuarter = (quarter: string) => {
    return (dispatch: Dispatch) => {
        dispatch({
            type: CHANGEQUARTER,
            payload: quarter
        })
    }
};

export const changeFilingStatus = (filingStatus: string) => {
    return (dispatch: Dispatch) => {
        dispatch({
            type: CHANGEFILINGSTATUS,
            payload: filingStatus
        })
    }
};

export const changeNumbersInput = (numbersInputValues: modelOneType) =>{
    return (dispatch: Dispatch) => {
        dispatch({
            type: CHANGENUMBERSINPUT,
            payload: numbersInputValues
        })
    }
};

export const changeAnnualization = (numbersAnnualizationValues: modelTwoType) =>{
    return (dispatch: Dispatch) => {
        dispatch({
            type: CHANGENUMBERSANNUALIZATION,
            payload: numbersAnnualizationValues
        })
    }
};

export const changePaymentsInput = (paymentsInputValues: modelThreeType) =>{
    return (dispatch: Dispatch) => {
        dispatch({
            type: CHANGEPAYMENTSINPUT,
            payload: paymentsInputValues
        })
    }
};

quarterReducer.ts

import { PayloadAction } from '@reduxjs/toolkit';
import { CHANGEQUARTER } from '../constants';

const initialState: string = "1";

const reducer = (state = initialState, action: PayloadAction<string>) => {
    switch(action.type){
        case CHANGEQUARTER:
            return state = action.payload;
        default:
            return state;
    };
}

export default reducer;

filingStatusReducer.ts

import { PayloadAction } from '@reduxjs/toolkit';
import { CHANGEFILINGSTATUS } from '../constants';

const initialState: string = "SINGLE";

const reducer = (state = initialState, action: PayloadAction<string>) => {
    switch(action.type){
        case CHANGEFILINGSTATUS:
            return state = action.payload;
        default:
            return state;
    };
}

export default reducer;

rootReducer.ts

import { combineReducers } from "redux";
import quarterReducer from "./quarterReducer";
import filingStatusReducer from "./filingStatusReducer";
import numbersInputReducer from "./numbersInputReducer";
import paymentsInputReducer from "./paymentsInputReducer";
import numbersAnnualizationReducer from "./numbersAnnualizationReducer";

const rootReducer = combineReducers({
    quarter: quarterReducer,
    filingStatus: filingStatusReducer,
    numbersInputValues: numbersInputReducer,
    paymentsInputValues: paymentsInputReducer,
    numbersAnnualizationValues: numbersAnnualizationReducer
});

export default rootReducer;

All help is appreciated. Please let me know if I need to clarify anything.


Solution

  • I'm a Redux maintainer. There's a few issues.

    First, the line return state = action.payload is probably not going to do what you want. If you're trying to replace the existing state, do return action.payload.

    The second is that even though you have Redux Toolkit installed, the rest of your logic is still a very outdated style of Redux code. With Redux Toolkit, you should be using the createSlice API to write your reducers. createSlice uses Immer inside, so that you can write "mutating" syntax in reducers, which gets turned into correct immutable updates. It also generates action creators for you, so you don't have to write any of those by hand.

    Please see our Redux docs tutorials, which explain how to use Redux Toolkit:

    Along with that, you're writing way too many unnecessary TS types. Instead of writing useSelector<RootState, string> everywhere, follow our steps for setting up "pre-typed" versions of useSelector and useDispatch that have the right state and dispatch types configured: