1

The problem I'm having is, that I have a useContext in which I provide all logged users. On the initial run of the app or when the users' log in the array gets populated with all the users that are currently on the server... Which works as expected. But I have also the functionality, that whenever the server "user-connected" event runs, the front-end should just push the user to the end of this array. And there lays the problem. From the backend, the right user is sent, but when I access the connectedUsers array, the array is empty... but it should be already populated.

UsersProvider.tsx

export const inState = {
  connectedUsers: [],
  addUser: (user: any) => {},
  sortUsers: (user: any, socketID: string) => {},
  setLoggedUsers: () => {},
};

export interface initState {
  connectedUsers: any[];
  addUser(user: any): void;
  sortUsers(users: any, socketID: string): void;
  setLoggedUsers: React.Dispatch < React.SetStateAction < any[] >> ;
}

const UsersContext = createContext < initState > (inState);

export const useUsers = () => {
  return useContext(UsersContext);
};

const initUserProps = (user: any) => {
  user.messages = [];
  user.hasNewMessages = false;
};

export const UsersProvider = ({
  children
}: Props) => {
  const [connectedUsers, setLoggedUsers] = useState < any[] > ([]);

  const addUser = (user: any) => {
    console.log('add', connectedUsers);
    // This is empty, but it should be already populated when next user connected.
  };

  const sortUsers = (users: any, socketUserID: string) => {
    const usersCopy = users;

    usersCopy.forEach((u: any) => {
      for (let i = 0; i < usersCopy.length; i++) {
        const existingUser = usersCopy[i];
        if (existingUser.userID === u.userID) {
          existingUser.connected = u.connected;
          break;
        }
      }
      u.self = u.userID === socketUserID;
      initUserProps(u);
    });
    // put the current user first, and sort by username
    let sorted = usersCopy.sort((a: any, b: any) => {
      if (a.self) return -1;
      if (b.self) return 1;
      if (a.username < b.username) return -1;
      return a.username > b.username ? 1 : 0;
    });

    setLoggedUsers([...sorted]);
  };

  return ( <
    UsersContext.Provider value = {
      {
        connectedUsers,
        setLoggedUsers,
        addUser,
        sortUsers
      }
    } >
    {
      children
    } <
    /UsersContext.Provider>
  );
};

And the part of ChatBoard.tsx, you can find addUser function initiated whenever user-connected happens. I really don't know why the would array be empty, if it is populated on the first run with users event.

const ChatBoard = (props: Props) => {
    const socket = useSocket();
    const {
      connectedUsers,
      setLoggedUsers,
      addUser,
      sortUsers
    } = useUsers();

    useEffect(() => {
      if (socket == null) return;

      socket.on('users', (users) => {
        console.log(users);
        if (socket.userID) {
          const socketID: string = socket ? .userID;
          sortUsers(users, socketID);
        }
      });

      socket.on('user-connected', (user: any) => {
        console.log(user, 'this user connected!');
        const connectingUser = user;
        addUser(connectingUser);
      });

      socket.on('user-disconnected', (userID) => {
        console.log('disconnected user');
        const users = [...connectedUsers];
        users.forEach((u) => {
          if (u.userID === userID) {
            u.connected = false;
            setLoggedUsers([...users]);
          }
        });
      });
      return () => {
        socket.off('users');
        socket.off('user-connected');
      };
    }, [socket]);

CodeSandbox

  • In `addUser()`, do you do something like: `setLoggedUsers([...connectedUsers, user])`? – Irfanullah Jan Oct 15 '21 at 15:24
  • I know that, but whenever I do that, the `connectedUsers` is empty, and it just adds the latest user... And I really don't see why it would be empty... As its already populated on `users` event, and the `sortUsers()` is called. The problem is not in updating it, but that the array is empty... – Žiga Grošelj Oct 15 '21 at 15:34

1 Answers1

1

So I have found the problem... so with React hooks sometimes a problem occurs called "Stale Closures", which means that React was picking up the old state (empty one, the one that was not yet populated and always returning that one.).

The solution to this problem, in my case is that when you use setState you use it with a callback. Like so, so you always get the latest state.

const addUser = (user: any) => {
    setLoggedUsers((oldUsers) => {
      const newUsers: any[] = [...oldUsers];
      console.log(newUsers);
      for (let i = 0; i < newUsers.length; i++) {
        const existingUser = newUsers[i];
        if (existingUser.userID === user.userID) {
          existingUser.connected = true;

          return newUsers;
        }
      }
      initReactiveProperties(user);
      newUsers.push(user);
      return newUsers;
    });
  };