Search code examples
reactjstypescriptredux

React-component is not re-rendered when the store is changed, neither automatically nor even by force update


This functional component should display a sorted list with checkboxes at each item that change the values in the store.

For some reason it is not re-rendered when the store is changed. And without a re-renderer, it (and the whole application) works very crookedly and halfway. I suspect that this is because the store object remains the same, albeit with new content. But I don’t understand how to fix it. I have even inserted a force update to the checkbox handler, but for some reason it does not work too.

Component:

import React, { useState, useReducer } from 'react';
import { ReactSortable } from 'react-sortablejs';
import ListItem from '@mui/material/ListItem';
import Checkbox from '@mui/material/Checkbox';
import { connect } from 'react-redux';
import { setGameVisible, setGameInvisible } from '../store/actions/games';

interface IGamesListProps {
  games: [];
  setGameVisible: (id: string) => void;
  setGameInvisible: (id: string) => void;
}

interface ItemType {
  id: string;
  name: string;
  isVisible: boolean;
}

const GamesList: React.FunctionComponent<IGamesListProps> = ({games, setGameVisible, setGameInvisible}) => {
  const [state, setState] = useState<ItemType[]>(games);

  // eslint-disable-next-line
  const [ignored, forceUpdate] = useReducer(x => x + 1, 0); // this way of force updating is taken from the official React documentation (but even it doesn't work!)

  const onCheckboxChangeHandle = (id: string, isVisible: boolean) => {
    isVisible ? setGameInvisible(id) : setGameVisible(id);
    forceUpdate(); // doesn't work :(((
  }

  return (
    <ReactSortable list={state} setList={setState} tag='ul'>
      {state.map((item) => (
        <ListItem
          sx={{ maxWidth: '300px' }}
          key={item.id}
          secondaryAction={
            <Checkbox
              edge="end"
              onChange={() => onCheckboxChangeHandle(item.id, item.isVisible)}
              checked={item.isVisible}
            />
          }
        >
          {item.name}
        </ListItem>
      ))}
    </ReactSortable>
  );
};

export default connect(null, { setGameVisible, setGameInvisible })(GamesList);

Reducer:

import { SET_GAMES, SET_GAME_VISIBLE, SET_GAME_INVISIBLE } from '../actions/games';

export const initialState = {
  games: [],
};

export default function games(state = initialState, action) {
  switch(action.type) {
    case SET_GAMES: {
      for(let obj of action.payload.games) {
        obj.isVisible = true;
      }
      return {
        ...state,
        games: action.payload.games,
      };
    }

    case SET_GAME_VISIBLE: {
      for(let obj of state.games) {
        if (obj.id === action.payload.id) {
          obj.isVisible = true;
        };
      }
      return {
        ...state,
      };
    }

    case SET_GAME_INVISIBLE: {
      for(let obj of state.games) {
        if (obj.id === action.payload.id) {
          obj.isVisible = false;
        };
      }
      return {
        ...state,
      };
    }
    
    default:
      return state;
  }
}

Thank you for any help!


Solution

  • Note: By the information You gave I came with the idea of the problem, but I posted here because it is going to be explanatory and long.
    • First of all, you don't pass the new game via mapStateToProps into Component in a state change, and even you do, useState won't use new game prop value for non-first render. You must use useEffect and trigger changes of the game and set the to state locally.

    At this point you must find the inner state redundant and you can remove it and totally rely on the redux state.

    const mapStateToProp = (state) => ({
      games: state.games // you may need to change the path
    })
    
     connect(mapStateToProp, { setGameVisible, setGameInvisible })(GamesList);
    
    • Second, the reducer you made, changes the individual game item but not the games list itself. because it is nested and the reference check by default is done as strict equality reference check-in redux state === state. This probably doesn't cause an issue because the outer state changes by the way, but I think it worth it to mention it.
    for(let obj of action.payload.games) {
      obj.isVisible = true; // mutating actions.payload.games[<item>]
    }
    return {
      ...state,
      games: [...action.payload.games], // add immutability for re-redenr
    };
    
    // or use map 
    return {
      ...state,
      games: action.payload.games.map(obj => ({...obj, isVisible:true})),
    };
    
    • Third, It's true your forceUpdate will cause the component to re-render, and you can test that by adding a console.log, but it won't repaint the whole subtree of your component including inner children if their props don't change that's because of performance issue. React try to update as efficiently as possible. Also you the key optimization layer which prevent change if the order of items and id of them doesn't change