How do I guarantee that reset is called before add using useState

Viewed 40

I created a custom hook

const useMessageStorage = () => {
  const [messages, setMessages] = useState([]);
  
  const addMessages = (newMessage) => {
    setMessages(oldMessages => [...oldMessages, newMessage)]);
  }

  const clear = () => {
    setMessages([]);
  }

  return { clear, addMessages }
}

Inside the consumer component, I want to do something like

const { clear, addMessages } = useMessageStorage();
...

clear() // clear before adding new messages
addMessage('new message') 

The above doesn't work because both calls are asynchronous. But I want to clear all messages before adding a new message in some specific scenario. How to tackle this problem? I considered using useRef but I'm afraid of using it because of race condition.

1 Answers

React state updates are asynchronously processed, this is true, but they are also processed in the order they are enqueued in. Since your addMessages callback uses a functional state update it will update from the previous state, not the state of the render cycle it was enqueued in.

// clear()
setMessages([]); // "draft" state: []

// addMessage(newMessage)
setMessages((oldMessages) => [ // updated from "draft" state: [newMessage]
  ...oldMessages,
  newMessage,
]);

Your code does work.

const useMessageStorage = () => {
  const [messages, setMessages] = React.useState([]);

  const addMessages = (newMessage) => {
    setMessages((oldMessages) => [...oldMessages, newMessage]);
  };

  const clear = () => {
    setMessages([]);
  };

  return { clear, messages, addMessages };
};

function App() {
  const [message, setMessage] = React.useState("");
  const { clear, messages, addMessages } = useMessageStorage();

  const addMessage = () => {
    if (message) {
      addMessages(message);
      setMessage("");
    }
  };

  const clearAndAddMessage = () => {
    if (message) {
      clear();
      addMessages(message);
      setMessage("");
    }
  };

  return (
    <div className="App">
      <label>
        Message:
        <input
          type="text"
          id="message"
          onChange={(e) => setMessage(e.target.value)}
          value={message}
        />
      </label>

      <button type="button" onClick={addMessage}>
        Add Message
      </button>
      <button type="button" onClick={clearAndAddMessage}>
        Clear & Add Message
      </button>

      <ul>
        {messages.map((message, i) => (
          <li key={i}>{message}</li>
        ))}
      </ul>
    </div>
  );
}

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

Related