skip to Main Content

recently I have stumbled upon a dilemma on how to properly approach UI updates. The stack I am using at work is React + axios. Let’s say there is a user profile object held in the local state and the user wants to update his first name. Which way is better? Please let me know what I am missing here?

    const handleUpdateUserProfile = async (newUserName) => {
      const originalUserName = userProfile.name;

      // Optimistically update the UI
      setUserProfile(prev => ({ ...prev, name: newUserName }));

      // Try to send the database request
      try {
        await axios.post("/some-endpoint", {...userProfile, name: newUserName });
      } catch (error) {
        // In case of an error go back to the previous value
        setUserProfile(prev => ({...prev, name: originalUserName}));
        console.error(error);
      }
    }

OR

    const handleUpdateUserProfile = async (newUserName) => {
      const originalUserProfile = structuredClone(userProfile);
      const updatedUserProfile = { ...originalUserProfile, name: newUserName }

      // Optimistically update the UI
      setUserProfile(updatedUserProfile);

      // Try to send the database request
      try {
        await axios.put("/some-endpoint", updatedUserProfile);
      } catch (error) {
        // In case of an error go back to the previous value
        setUserProfile(originalUserProfile);
        console.error(error);
      }
    }

I am aware that I should be also checking for the status code in the response object from the post request.

Thanks for all the suggestions.

2

Answers


  1. Least code approach, will be to simply defer the update until the API is completed and successful. You can show a spinner while the update happens.

    const handleUpdateUserProfile = (newUserName) => {
      setLoading(true)
      const originalUserProfile = structuredClone(userProfile);
      const updatedUserProfile = { ...originalUserProfile, name: newUserName }
    
        await axios.put("/some-endpoint", updatedUserProfile).then((response: any) => {
          if(response.status === 'SUCCESS') {
            setUserProfile(updatedUserProfile);
          }
          setLoading(false)
        }).catch(error =>  {
          // In case of an error go back to the previous value
          setLoading(true)
          console.error(false);
        }
    }
    
    Login or Signup to reply.
  2. While reviewing the codes based on the things happening under the hood, it found a few comments and suggestions as listed below.

    a) The first code uses an updater function in the below two calls of the state updater. This may not be an essential requirement although it will work. The point is that there will be two separate renders with respect to the two calls below. This is due to the fact that React will not or cannot batch under the event of an async code. Since no batching is going to happen, and it is going to be rendered individually, therefore there is no need to use an updater function as it is in this code. An updater function is mandatory when there will be a series of state updates in the same event which is not the case over here.

    setUserProfile(prev => ({ ...prev, name: newUserName }));
    setUserProfile(prev => ({...prev, name: originalUserName}));
    

    Therefore the essential code required for this case is quite correct in the second code. This code uses an expression or “replace with value” instead of an updater function. It does not look for the state values already queued up in the event as nothing being queued up in this case, instead it just replaces the state with the given values which is the exact need of this case. Again the reasoning for this suitability is that there are two separate renders for this event – one for the sync code and the other for async code in it.

    setUserProfile(updatedUserProfile);
    setUserProfile(originalUserProfile);
    

    As an aside, the key point in this case is not at all the variable prev, but the local variables originalUserName and originalUserProfile with respect to the two codes.

    To generalize the point, the first state update call in any event should not really require an updater function. It can be a simple expression or “replace with value”. However, to execute a series of state updates, an updater function is a must.

    A sample code based on the above points given below.

    a) Please note that the code logs two logs in the console showing that there are two separate renders and no batching.

    b) The local variable tempA is vital as far as the error case is concerned.

    c) On clicking the button, the sync state updater updates the state to 1 and then on error, the async state updater reverts this change by setting 0. It gets this old value from the local variable tempA.

    App.js

    import { useState } from 'react';
    
    export default function App() {
      const [a, setA] = useState(0);
    
      async function handleClickReplacewithAwait() {
        const tempA = a;
        setA(1);
        try {
          await new Promise((resolve, reject) => setTimeout(reject, 1000));
        } catch (err) {
          setA(tempA);
        }
      }
    
      console.log(`rendered ${a}`);
      return (
        <>
          {a}
          <br />
          <button onClick={handleClickReplacewithAwait}>
            Click to Add 1 and Revert
          </button>
        </>
      );
    }
    

    Test run:

    On loading the app:

    On loading the app

    After clicking the button:

    After clicking the button

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search