Search code examples
javascriptreactjsreact-hooksreact-hook-form

Rendering UI in React JS


I recently learnt that React renders a snapshot of the UI before the set function is called to change the state and re-rendering is perform. The entire JSX works with the previous state. For example:

import { useState } from 'react';

export default function FeedbackForm() {
  const [name, setName] = useState('');

  function handleClick() {
    setName(prompt('What is your name?'));
    alert(`Hello, ${name}!`);
  }

  return (
    <button onClick={handleClick}>
      Greet
    </button>
  );
}

After giving a name in the prompt box, the alert still says : Hello, !. It is because the alert takes name from the first rendering which is null.

According to the above description this code:

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
      alert(`You said ${message} to ${to}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

So as onchange event fires, the state of both the variable to and message changes, and when the form is submitted, the alert should show : You said Hello to Alice rather it shows alert which includes new to and new message. Where I am getting wrong?


Solution

  • Your understanding of the React rendering lifecycle and state management isn't quite right. The onchange event fires whenever the input changes. When that event is triggered, it asynchronously queues the state to update via setTo and setMessage. So long as nothing is blocking the event loop, it will update the state and re-render (if there is anything to re-render). The state is actually updated in a following event loop cycle, but this will execute in the background before you've had a chance to notice it. It does not technically update within your onchange routine. In your second example, there isn't anything to actually re-render so you aren't seeing changes in the UI. However, the state IS updating.

    The reason you're seeing Hello, ! in the first example is because the handleClick method blocks the event loop until it completes, which means the state of name won't actually update until a following event loop cycle.

    In short, the event loop is executing multiple cycles while you're manipulating the UI input fields, giving React time to update the state variables. By the time you trigger an event for submitting the form, you can be assured that any prior changes to the state have completed.

    If you want to see this in a bit of a delayed fashion, you could simply change your onchange event handler to the following:

    {e => {console.log(to); console.log(message); setMessage(e.target.value)}}
    

    In the above case, you'll see the previous state of the to and message variables before they've been updated. If you make several changes to trigger multiple console logs, then you'll see that the state is indeed updated before the form is submitted.

    You "queue" changes to state by enqueuing a new event to the event loop. JavaScript is single threaded, so the event loop dequeues whatever event is next in line. But if you're just clicking on elements in the DOM, the event loop is assuredly executing events in the background, because it would be wildly inefficient to block until you've clicked a particular button to process all the state changes.

    Here is a full example that will show you how the state is truly updating on each onchange event.

    import { useState } from 'react';
    
    export default function Form() {
      const [to, setTo] = useState('Alice');
      const [message, setMessage] = useState('Hello');
    
      function handleSubmit(e) {
        e.preventDefault();
          alert(`You said ${message} to ${to}`);
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <label>
            To:{' '}
            <select
              value={to}
              onChange={e => setTo(e.target.value)}>
              <option value="Alice">Alice</option>
              <option value="Bob">Bob</option>
            </select>
          </label>
          <textarea
            placeholder="Message"
            value={message}
            onChange={e => setMessage(e.target.value)}
          />
          <h1>To: {to}</h1>
          <p>Message: {message}</p>
          <button type="submit">Send</button>
        </form>
      );
    
    

    The addition of the h1 and p are not necessary since the form inputs literally are using the current state values of the to and message variables, but putting those state values outside of the input may help you see what is happening.

    I wanted to now how the second code doesn't return : You said Hello to Alice.

    Because by the time the form is submitted, the state changes have processed. This is because the onchange event updates the state asynchronously and those asynchronous events will complete before the form submit event is executed.