2

I'm trying to build a chat application, and I have a function for selecting users you want to add to the chat. It is in the following component (I've left out the import statements and one of the functions for brevity):

const AddUserBox = (props) => {

  // context:
  const { friends, getFriends, user, whoToMsg } = useContext(AppContext);

  // state:
  const [friendsDisplay, setFriendsDisplay] = useState();
  const [selected, setSelected] = useState();

  const getFriendsDisplay = async function() {
    if (!friends) await getFriends(user.username);

    if (friends) {
      let currentFriend;
      let result;
      let friendsInfo = [];

      friends.sort();

      for (let i = 0; i < friends.length; i++) {

        if (friends[i].user_1 === user.username) {
          currentFriend = friends[i].user_2;
        } else {
          currentFriend = friends[i].user_1;
        }

        if (currentFriend === whoToMsg) {
          return;
        } else if (typeof whoToMsg === Array) {
          if (whoToMsg.includes(currentFriend)) return;
        }

        await fetch('/proxy/get-user-update', {
          method: 'POST',
          headers: {
            'Accept': 'application/json, text/plain, */*',
            'Content-Type':'application/json'
          },
          body: JSON.stringify({username: currentFriend})
        })
        .then(results => results.json())
        .then(data => result = data)
        .catch(err => console.error(err));

        friendsInfo.push(result);
      }

      setFriendsDisplay(friends.map((friend, i) => {
        return <div 
          className="add-user-friends-button" 
          key={i}
          i={i}
          onClick={(e) => {selectUser(e, friend)}}
        >
          {friendsInfo[i].name}
        </div>
      }));
    }
  }

  const selectUser = function(e, friend) {
    let newArr = [];
    let currentFriend;

    if (friend.user_1 === user.username) {
      currentFriend = friend.user_2;
    } else {
      currentFriend = friend.user_1;
    }

    if (selected) {
      newArr = [...selected];
      if (selected.includes(currentFriend)) {
        e.target.style.backgroundColor = "";
        newArr.splice(newArr.indexOf(currentFriend), 1);
        setSelected(newArr);
        return;
      }
    }

    newArr.push(currentFriend);
    newArr.sort();

    e.target.style.backgroundColor = "#E6FCFF";
    setSelected(newArr);
    console.log(selected);
  }

  return ( 
    <div className="add-user">
      <span>Add users to chat:</span>
      <div className="add-user-friends">
        {friendsDisplay}
      </div>
      <button onClick={() => props.close()}>Close</button>
      <button onClick={() => addUsers()}>Add users</button>
    </div>
   );
} 

The idea is that you select a user from a list and it is added to an array of selected users so that when they click the add users button the users are added to the chat.For some reason setSelected(newArr) does not set the state to the new array. Can anyone help me understand / fix this?

Chris Capua
  • 37
  • 1
  • 4

2 Answers2

1

Since setState is an async method, you don't always get the expected value when you call it right away. You can pass a callback method to your setState. If you move your console.log statement into there, you should see your expected results.

There's some information here: https://dev.to/dance2die/accessing-react-state-right-after-setting-it-2kc8

Jessica Hall
  • 11
  • 1
  • 2
  • the issue isn't just the console.log statement not showing the updated state. it seems like it actually doesn't update the state, because I can't deselect a user that's been selected, either. (```if (selected)``` always returns false) – Chris Capua Sep 13 '19 at 19:02
  • @Chris-Capua Did you try to debug the program to investigate the issue? – Jeroen Heier Sep 13 '19 at 19:09
1

The main thing awry here is that the value of selected will not be updated immediately after calling setSelected() (see post link to thread below).

Instead of trying to access the most recent state within a callback, use useEffect. Setting your state with the function returned from setState will not immediately update your value. The state updates are batched and updated on the next render.

It may help if you think of useEffect() like setState's second parameter (from class based components).

If you want to do an operation with the most recent state, use useEffect() which will be hit when the state changes:

const {
  useState,
  useEffect
} = React;

function App() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count-1);
  const increment = () => setCount(count+1);
  
  useEffect(() => {
    console.log("useEffect", count);
  }, [count]);
  console.log("render", count);
  
  return ( 
    <div className="App">
      <p>{count}</p> 
      <button onClick={decrement}>-</button> 
      <button onClick={increment}>+</button> 
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Some further info on useEffect()

Miroslav Glamuzina
  • 4,472
  • 2
  • 19
  • 33
  • 1
    thank you for your thoughtful answer. I will be re-reading and checking the link you provided here shortly. I've updated the post with the function where selectUser is actually called. – Chris Capua Sep 13 '19 at 19:06
  • I'm calling setSelected twice because one is in a conditional block and the other is not. I am trying to remove the deselected user or add the selected user appropriately. – Chris Capua Sep 13 '19 at 20:21
  • My mistake, I overlooked the return there, although the the answer below still applies to your question. Is there something else wrong? – Miroslav Glamuzina Sep 13 '19 at 22:52