Search code examples
reactjsreduxreact-hooksreact-reduxredux-thunk

Correct way to update object value of react state in reducer


In below code React functional component is re-rendering for all cases except UPDATE_LNH (if clause only) case. Looking at the references over internet it seems the root cause behind this behavior is immutability of state object where original reference stays same for UPDATE_LNH case hence it won't re-render the component. What is the correct way to update state here so that it will re-render dependent functional component?

import {
    DELETE_LNH,
    NavigationDispatchTypes,
    Output,
    OutputType,
    RETRIEVE_LNH,
    SET_ACTIVE_ITEM,
    UPDATE_LNH
} from "../actions/navigation/NavigationTypes";

interface DefaultStateI {
    outputs?: Output[],
    activeOutput?: any
}

const defaultState: DefaultStateI = {
};
const navigationReducer = (state: DefaultStateI = defaultState, action: NavigationDispatchTypes): DefaultStateI => {
  switch (action.type) {
      case RETRIEVE_LNH:
          return {...state, outputs: action.payload}
      case UPDATE_LNH: {
          let opId = action.payload[0].guid;
          let opIndex = state.outputs.findIndex(i => i.guid == opId);
          debugger;
          if (opIndex > -1) {
              state.outputs[opIndex] = action.payload[0]
          } else {
              state.outputs.unshift(action.payload[0]);
          }
          return {...state};
      }
      case SET_ACTIVE_ITEM: {
          state.activeOutput = action.payload;
          return {...state}
      }
      case DELETE_LNH: {
          state.outputs.shift();
          return {...state}
      }
      default:
          return state
  }
};

export default navigationReducer;

This is how my dependent functional component looks like:

export function Navigation() {
  const navigationState = useSelector((state: RootStore) => state.navigation);
  return (
    navigationState?.outputs && (
      <>
        <div>
          <p>Hello World!</p>
        </div>
      </>
    )
  );
}

    

Store.ts looks like:

import {applyMiddleware, createStore} from "redux";
import RootReducer from "./reducers/RootReducer";
import {composeWithDevTools} from "redux-devtools-extension";
import thunk from "redux-thunk";

const loadState = () => {
    try {
        const serializedState = sessionStorage.getItem('state');
        if (serializedState === null) {
            return undefined;
        } else {
            return JSON.parse(serializedState);
        }
    } catch (err) {
        // Error handling
        console.log(err)
        return undefined;
    }
}


const store = createStore(RootReducer,loadState(),composeWithDevTools(applyMiddleware(thunk)))

window.onbeforeunload = (e) => {
  const state = store.getState();
  saveState(state);
};

const saveState = (state: any) => {
  try {
    const serializedState = JSON.stringify(state);
    sessionStorage.setItem('state', serializedState);
  } catch (err) {
    // ...error handling
      console.log(err)
  }
};

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

export default store;

export const RETRIEVE_LNH = "RETRIEVE_LHN";
export const DELETE_LNH = "DELETE_LNH";
export const UPDATE_LNH = "UPDATE_LNH"
export const SET_ACTIVE_ITEM = "SET_ACTIVE_ITEM";
export const NOTIFY_RESULT = "NOTIFY_RESULT";
export const CLEAR_RESULT = "CLEAR_RESULT";
export const ACTIVATE_LOADER = "ACTIVATE_LOADER";
export const DEACTIVATE_LOADER = "DEACTIVATE_LOADER";

export enum OutputType {
    test = 1,
    notest,
    Error
}

export enum OperationStatus {
    Success,
    Fail
}

export class OperationResult {
    status: OperationStatus;
    message: string;
}

export interface OutputParam {
    ar_id?: any;
    as_id?: number;
}

export interface Output {
    outputName: string;
    outputType: OutputType;
    outputParams?: OutputParam;
    dateCreated?: string;
    showNewIcon: boolean;
    guid: any;
    showLoader: boolean;
}

export interface GetLNH {
    type: typeof RETRIEVE_LNH,
    payload: Output[]
}

export interface UpdateLNH {
    type: typeof UPDATE_LNH,
    payload: Output[]
}

export interface DeleteLNH {
    type: typeof DELETE_LNH
}

export interface NotifyOperation{
    type: typeof NOTIFY_RESULT,
    payload: OperationResult
}

export interface ClearOperation{
    type: typeof CLEAR_RESULT,
    payload: OperationResult
}

export interface SetActiveItem{
    type: typeof SET_ACTIVE_ITEM,
    payload: string
}

export interface ActivateLoader{
    type: typeof ACTIVATE_LOADER,
    payload: boolean
}

export interface DeactivateLoader{
    type: typeof DEACTIVATE_LOADER,
    payload: boolean
}

export type NavigationDispatchTypes = GetLNH | SetActiveItem | NotifyOperation | ActivateLoader | DeactivateLoader
    | DeleteLNH | ClearOperation | UpdateLNH

Solution

  • For your example, you can't just do state.outputs[opIndex] = , you have to clone the state first and clone state.outputs array too like this:

    let opId = action.payload[0].guid;
    let opIndex = state.outputs.findIndex(i => i.guid == opId);
    const newState = {...state,outputs: [...state.outputs]};
    if (opIndex > -1) {
        newState.outputs[opIndex] = action.payload[0]
    } else {
        newState.outputs.unshift(action.payload[0]);
    }
    return newState;
    

    Advice

    If you can, use redux-toolkit. It's way easier to do Redux with it and will save you a lot of clones, state.outputs[opIndex] = action.payload[0] will work when using redux-toolkit because immer library is used underneath.