Possible for input/useState not to be set on button click?

Viewed 288

Do I need to worry that fetchClassesSearchedByName(searchName); will not have a searchName value? Is it possible my button click is faster than searchName being set?

// On input
const [searchName, setSearchName] = useState('');

// Text input
<input
    type='text'
    value={searchName}
    onChange={(e) => setSearchName(e.target.value)}
/>

// On button click
const searchNameBtn = (e) => {
        fetchClassesSearchedByName(searchName);
}

Thank you

2 Answers

setState is asynchronous, so it is possible if you've got the reflexes of an old west gunslinger.

Your default value is '', though, so I don't think you'll need any error handling as long as fetchClassesSearchedByName can take an empty string.

Do I need to worry that fetchClassesSearchedByName(searchName); will not have a searchName value?

You have defined initial searchName state and all the setSearchName calls pass valid defined values. And so long as all state updates are valid defined values, there's no chance for fetchClassesSearchedByName to be called without a defined searchName value.

Is it possible my button click is faster than searchName being set?

Yes, this is entirely possible, BUT also entirely unrealistic. It's possible because React state updates are not immediate, but are enqueued and asynchronously processed.

Consider the follow example. There are two inputs with onChange handlers.

  • Input and handler #1: The first input attempts to simulate clicking the button immediately after enqueueing a state update, but due to the way Javascript enclosures and the event loop work, the button's onClick event is processed before React processes the state update because it's synchronous within the onChange handler function's scope. The fetchClassesSearchedByName appears to be a "render cycle behind" because it's logging the unupdated state.
  • Input and handler #2: The second input attempts to simulate clicking the button immediately after enqueueing a state update, but uses a setTimeout of 0. This places the click event callback at the end of the event queue. Note that this is the earliest possible the button can even be clicked after the state update was enqueued. React's state update is processed, then the click event is processed. Note here that the fetchClassesSearchedByName now always sees the updated React state.

const fetchClassesSearchedByName = (value) => console.log(value);

function App() {
  const buttonRef = React.useRef();
  const [searchName, setSearchName] = React.useState("");

  const changeHandler1 = (e) => {
    setSearchName(e.target.value);
    buttonRef.current.click();
  };

  const changeHandler2 = (e) => {
    setSearchName(e.target.value);
    setTimeout(() => {
      buttonRef.current.click();
    }, 0);
  };

  const searchNameBtn = (e) => {
    fetchClassesSearchedByName(searchName);
  };

  return (
    <div className="App">
      <div>
        <label>
          Input 1 w/ synchronous button click
          <input type="text" value={searchName} onChange={changeHandler1} />
        </label>
      </div>
      <div>
        <label>
          Input 2 w/ asynchronous button click
          <input type="text" value={searchName} onChange={changeHandler2} />
        </label>
      </div>

      <div>
        <button ref={buttonRef} type="button" onClick={searchNameBtn}>
          Click me
        </button>
      </div>
    </div>
  );
}

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

Related