-1

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
Pritam
  • 1,288
  • 5
  • 23
  • 40
  • Your functional component code misses a lot of details like from where does `navigationState` come for example, you should include the `connect` or `useSelector` code too – niceman Aug 19 '22 at 02:57
  • @niceman Updated the required details. Thanks! – Pritam Aug 19 '22 at 04:52
  • And where does `globalState?.globalData` come from? – niceman Aug 19 '22 at 14:51
  • Well I just removed that dependency but it's still not rendering after array update:( – Pritam Aug 19 '22 at 15:27
  • Could you post `DefaultStateI` and `NavigationDispatchTypes` code? And from now on please post a [Minimal Reproducible Example.](https://stackoverflow.com/help/minimal-reproducible-example) – niceman Aug 19 '22 at 15:31

1 Answers1

2

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.

niceman
  • 2,653
  • 29
  • 57
  • Thanks @niceman for helping out here! Just wanted to check if doing only npm install redux-toolkit would solve the problem? or do I need to do some code / configuration changes? – Pritam Aug 18 '22 at 21:51
  • @Pritam of course you'll need code / configuration changes, that's why I said “If you can” – niceman Aug 18 '22 at 21:55
  • Thanks @niceman for reverting back. Well I tried first suggestion of cloning but my functional component still did not re-rendered. I have updated question with my functional component code. Can you please have look at same? – Pritam Aug 18 '22 at 22:43
  • Apart from updating the state mentioned in this answer, forcefully re-rendering the component helped to resolve the issue. https://stackoverflow.com/questions/46240647/react-how-to-force-a-function-component-to-render – Pritam Aug 25 '22 at 13:26