Search code examples
javascriptreactjsuse-statepreact

React doesn't re-render after State change on button click in ReactJS (preact)


I have the following code which is working except that it doesn't re-render the page after pressing a button.

I have two different buttons being displayed depending on wether a user is part of a room or not. If a user is part of a room they can click on LEAVE which will execute an API call. I would then like the component to reload and for the button to display JOIN (since they are no longer part of that room).

import JoinedRooms from '../../components/matrix_joined_rooms';
import JoinRoom from '../../components/matrix_function_join_room';
import LeaveRoom from '../../components/matrix_function_leave_room';
import { useEffect, useState } from 'preact/hooks';

const JoinLeaveButton = ({ name, roomId }) => {
  const joinedRooms = JoinedRooms();
  const [x, setX] = useState(5);

  useEffect(() => console.log("re-render because x changed:", x), [x])

  const handleXClick = (number) => {
    setX(number)
  }

  if (joinedRooms.includes(name)) {
    return <button name={roomId} onClick={() => {
      LeaveRoom(roomId);
      handleXClick(10);
    }
    }>LEAVE</button>
  } else {
    return <button name={roomId} onClick={() => {
      JoinRoom(roomId);
      handleXClick(20);
    }
    }>JOIN</button>
  }
}

export default JoinLeaveButton;

my JoinRoom and LeaveRoom components are a simple API call which look like this:

const JoinRoom = (roomId) => {
  fetch(`https://example.com/_matrix/client/r0/rooms/${roomId}/join`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${localStorage.getItem("mx_access_token")}`
    }
  });
}

export default JoinRoom;

The funcionality of the button itself works, the problem is that I have to manually reload the page for it to be displayed correctly.

I have put a dummy state in place which is executed whenever you press the button and it also logs to the console properly. I was under the impression, that changing a state should re-render a component in React (or in this case preact).

Thanks!


Solution

  • In essense: you need to store the state of the joined rooms somewhere and have that state updated everytime a user joins or leaves a room.

    I've gone way overboard here, but a custom hook like this makes a lot of sense:

    // api calls
    const fetchRooms = async userid => { ... }
    const joinRoom = async (userId,roomId) => { ... }
    const leaveRoom = async (userId,roomId) => { ... }
    
    // custom hook
    const useRooms = (userId) => {
       const [fetching,setFetching] = useState(true);
       const [error,setError] = useState(false);
    
       // joinedRooms state is an empty array when this hook is first used
       // it will be updated later using the useEffect hook
       // or via the join or leave functions below
       const [joinedRooms,setJoinedRooms] = useState([]);
    
       // when the component that uses this hook is mounted
       // or the user id changes, update the state
       useEffect(() => {
           let canceled;
    
           setFetching(true);
           (async() => {
              try {
                 const rooms = await fetchRooms(userId);
                 canceled || setJoinedRooms(rooms);
              } catch(err) {
                 canceled || setError(error);
              } finally {
                 canceled || setFetching(false);
              }
           })();
           return () => canceled = true;
       },[userId]);
    
       const leave = async roomId => {
         try {
            await leaveRoom(userId,roomId)
            // alternatively you could fetch all the user rooms again here
            setJoinedRooms(joined => joined.filter(r => r !== roomId));
         } catch(err) {
           // couldn't leave the room - what do you want to do with the state?
         }
       }
    
       const join = async roomId => {
         try {
            await joinRoom(userId,roomId);
            // alternatively you could fetch all the user rooms again here
            setJoinedRooms(joined => [...joined,roomId]);
         } catch(err) {
           // couldn't join the room - what do you want to do with the state?
         }
       }
    
       return {
          fetching,
          error,
          joinedRooms,
          leave,
          join
       }
    
    
    }
    

    In a component you'd use it something like this:

    const Rooms = (userId,listOfAllYourRoomIds) => {
       const { joinedRooms, fetching, error, join, leave } = useRooms(userId);
    
       // the `join` and `leave` functions are what you'll call 
       // when a user wants to join or leave a room, the joinedRooms prop will get
       // updated according, and everything will "just work"
    
       return listOfAllYourRoomIds.map(roomId => <SomeRoomComponent roomId={roomId}/>)
       
    }