Nerah
Nerah

Reputation: 66

React component not detecting change in redux state

I'm doing a chat service.

enter image description here

Everything is working fine, expect the following functionality :

To do so, I tried the following reducer :

case NEW_CHAT_MESSAGE:
      const setRoomFirst = (array, roomId) => {
        const chatIndex = array.findIndex((arr) => arr._id === roomId);
        array.push(...array.splice(0, chatIndex));
        return { arr: array, hasBeenUpdated: chatIndex !== -1 };
      };

      const rooms = state.rooms;

      // move room to first position
      const updatedRooms = setRoomFirst(rooms, action.payload.message.chatRoom);

      if (updatedRooms.hasBeenUpdated) {
        // will always be the first room
        updatedRooms.arr[0].messages.push(action.payload.message);
      }

      return { ...state, rooms: updatedRooms.arr };
    default:
      return state;
  }

The component that will manage the state of the chat :

function Chat() {
  const classes = useStyle();
  const dispatch = useDispatch();

  const callback = useCallback(
    (rooms) => {
      console.log("dispatch");
      dispatch(newChatMessage(rooms));
    },
    [dispatch]
  );

  const { roomId } = useParams();
  const rooms = useSelector(ChatRoomsSelector);

  const [selectedRoom, setSelectedRoom] = useState(
    roomId || (rooms.length ? rooms[0]._id : null)
  );

  const oldRoomsValue = usePrevious(rooms);

  useEffect(() => {
    console.log("rooms has changed", rooms);
    if (!selectedRoom) {
      setSelectedRoom(roomId || (rooms.length ? rooms[0]._id : null));
    }
    if (oldRoomsValue !== rooms) {
      console.log("a change in rooms has been detected");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rooms]);

  const onSelect = useCallback((roomId) => {
    setSelectedRoom(roomId);
  }, []);

  const current = rooms.filter((room) => room._id === selectedRoom)[0];

  return (
    <Container>
      <Typography variant="h1" className={classes.title}>
        Messagerie
      </Typography>
      <Grid container spacing={2}>
        <Grid item xs={4}>
          <ChatListContainer
            rooms={rooms}
            roomSelected={selectedRoom}
            onSelect={onSelect}
          />
        </Grid>
        <Grid item xs={8}>
          {current && <ChatContentContainer room={current} />}
        </Grid>
      </Grid>
    </Container>
  );
}

export default Chat;

Thank you in advance for your help! :)

Upvotes: 1

Views: 388

Answers (2)

HMR
HMR

Reputation: 39320

You are mutating the redux state and you did not indicate you are using immer so that's something you should not do:

case NEW_CHAT_MESSAGE:
  const {
    payload: { message },
  } = action;
  //find or create room
  const room = state.rooms.find(
    (room) => room._id === message.chatRoom
  ) || { _id: message.chatRoom, messages: [] };
  //add message to room
  const roomWithMessage = {
    ...room,
    messages: [...room.messages, message],
  };
  return {
    ...state,
    //set room as first in rooms
    rooms: [
      //room with message is the first
      roomWithMessage,
      //other rooms without the room with message
      ...state.rooms.filter(
        (room) => room._id !== roomWithMessage._id
      ),
    ],
  };

Your effect has missing dependencies, this can cause some problem when you want the effect to run and I found some other inconsistancies (see comments in code):

function Chat() {
  const dispatch = useDispatch();

  // your code does not show how you need to use this
  //  if this is to dispatch a new chatmessage then the
  //  paremeter passed would not be rooms but message {chatRoom:roomid, ...other}
  // const callback = useCallback(
  //   (rooms) => {
  //     console.log('dispatch');
  //     dispatch(newChatMessage(rooms));
  //   },
  //   [dispatch]
  // );

  const { roomId } = useParams();
  const rooms = useSelector(ChatRoomsSelector);

  const [selectedRoom, setSelectedRoom] = useState(
    //use optional chaining for cleaner syntax:
    //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
    roomId || rooms?.[0]?._id
  );
  //use Array.prototype.find if you want to find one item
  const current = rooms.find(
    (room) => room._id === selectedRoom
  );

  useEffect(() => {
    //a console.log does not show what you want to do here
    //  the only thing you actually did is change selectedRoom
    //  when current room changes and no selectedRoom was provided
    setSelectedRoom(selectedRoom=>selectedRoom || current?._id);
  }, [current]);

  const onSelect = useCallback((roomId) => {
    setSelectedRoom(roomId);
  }, []);

  return 'jsx';
}

Upvotes: 1

Nerah
Nerah

Reputation: 66

Thank you so much HMR for your answer ! I would give you a hug if I had a chance. ^^

Just one thing to correct : I needed to enumerate the result of filter in the reducer to make it work.

return {
        ...state,
        // set room as first in rooms
        rooms: [
          // room with message is the first
          roomWithMessage,
          // other rooms without the room with message
          ...state.rooms.filter((room) => room._id !== roomWithMessage._id),
        ],
      };

Upvotes: 1

Related Questions