Exploring React 19: Impact on Users Already Utilizing Data Management Libraries like React Query and SWR

React 19 has come up many new features mainly focused on the way we handle data. Yes, Now React provides many hooks to handle effective data fetching as well as data mutation. Let's quickly list down all of them here.
New Hooks:

  1. useActionState()

  2. useFormStatus()

  3. useOptimistic()

  4. Async function support for startTransition() which is called Action.

Many enhancements:

  1. use() API

  2. Default value support for useDeferredValue()

  3. Support for Meta Tags inside components

  4. Cleanup functions for ref function

  5. ref as a prop

Introduction of Action:

Action in React is anything we want to perform after the users interacted with UI.
Eg: Clicking the button is Action. Submitting the Form is an Action and list goes on.

Earlier we used to do it in different way by combining multiple hooks to name a few useEffect, useState, useMemo.

But now React has brought up intuitive API so that we do not need more boiler plate code to solve that problem.

Let's take the simple example of saving user details:

sample of without action:

function UpdateUser() {
    const [status, setStatus] = useState("idle");
    const saveUser = (userData) => {
        setStatus("saving");
        service.saveUser(userData)
        .then(() => {
            setStatus("idle");
            redirect("/userdetail");
        }).catch(() => {
            setStatus("error");
        })
    }

   return (
        <>
            {status == "saving" ? "Saing...." : ""}
            <button onClikc={() => saveUser()}>save user</button>
        </>
   )
}

sample with action:

function UpdateUser() {
    const [isPending, startTransition] = useTransition();
    const [error, setError] = useState();
    const saveUser = (userData) => {
        startTransition(async () => {
            await service.saveUser(userData)
                .then(() => {
                   redirect("/userdetail");
                })
                .catch(err => {
                    setError(err);
                });
        });
    }

   return (
        <>
            {isPending ? "Saing...." : ""}
            <button onClikc={() => saveUser()}>save user</button>
        </>
   )
}

By wrapping the action, React has introduced two hooks: useActionState, useOptimistic.

useActionState Hook:

This hook used to handle the states of data mutation, eg: sending data to server. For example, here is a simple case of updating profile info.

import { useActionState } from 'react';

export function MemberForm() {
  const [error, submitAction, isPending] = useActionState(
    async (previewState, formData) => {
      const error = await updateUser(formData).catch(
          (err) => 'Error ' + previewState
      );
      if(!error) {
        redirect("/memberdetail");
        return null;
      }
      return error;
    }
  );

  return (
    <form id="memberForm" name="memberForm" action={submitAction}>
      {isPending && <p>Updating ...</p>}
      <fieldset>
        <label htmlFor="firstName">First Name</label>
        <input id="firstName" name="FirstName" type="text" />
      </fieldset>
      <fieldset>
        <label htmlFor="lastName">Last Name</label>
        <input id="lastName" name="LastName" type="text" />
      </fieldset>
      <!-- implementation below -->
      <PhoneNumber />
      <button type="submit">Save</button>
      {error && <p>{error} </p>}
    </form>
  );
}

The action function provided to useActionState() should return null or error upon performed action and form will get reset automatically. The previewState will be whatever we returned from action.

useFormStatus Hook:

With this hook we can know the form submission status within form components.

function PhoneNumber() {
  const { pending } = useFormStatus();
  return (
     <fieldset disabled={pending}>
       <label htmlFor="Phone">Phone</label>
       <input id="Phone" name="Phone" type="number" />
     </fieldset>
  )
}

useOptimistic() Hook:

This hook used to perform the optimistic update to the UI while we are waiting for the response from the server. Eg: when hitting Like button, we don't want to wait until the server respond with success status to show the Like reaction. We can show the reaction immediately and once the response came from the server; we will synchronise it to our local state. This is what is called optimistic update.

Earlier we have to keep track of states of API and manually switch back to old value just in case API got failed. With this hook, our code will be less boiler plate. Here is the simple example of Like button.

import React from 'react';

function LikeButton({ count, onClick }) {
  // creating optimistic state for Like count.
  const [optimisticCount, setOptimisticCount] = React.useOptimistic(
    count,
    (state, newValue) => {
      // Here is where we calculate the immdiateState.
      // return value of this state function will be immediately
      // set to optimisticCount
      console.log('optimistic callback calling ', state, newValue);
      return newValue;
    }
  );

  // creating the transition for Like state update because 
  // setOptimisticCount method can only be called inside Action.
  // Actions are simple async function inside startTransition() method.
  const [isPending, startTransition] = React.useTransition();

  const updateCount = () => {
    startTransition(async () => {
      // immediate incrementing the count 
      setOptimisticCount(optimisticCount+1);
      // wait until the values get saved in parent component
      await onClick(optimisticCount).catch((err) => console.log(err));
      // This transition will be in pending states untill it returns
      return null;
    });
  };

  return (
    <p>
      <span>
        {optimisticCount} {count !== optimisticCount ? 'updating...' : ''}
      </span>
      <button onClick={updateCount}>Like</button>
    </p>
  );
}

export function ReactionComponent() {
  const [count, setCount] = React.useState(0);
  const onClick = () => {
    return new Promise((resolve, reject) => {
     // mimiking the delay
      setTimeout(() => {
        setCount(count + 1);
        reject();
      }, 2000);
    });
  };
  return <LikeButton count={count} onClick={onClick} />;
}

use API:

use used to consume resources inside components. The resources can be

  1. React Context:

    Here is the simple utility component and hook to maintain user session.

     import React from 'react';
    
     const UserContext = React.createContext();
    
     export function UserSession({ children }) {
       const user = React.useMemo(() => ({ id: '001', name: 'user' }), []);
       return <UserContext value={user}>{children}</UserContext>;
     }
    
     export function useUser() {
       return React.use(UserContext);
     }
    
  2. Promises

    By using this feature, we can directly load data inside component, no need to for extra boilerplate code. Here is the simple example how we used to do before this feature.

    Before:

     //Before
     function UserDetailComponent({ userId }) {
         const [user, setUser] = useState(null);
         useEffect(() => {
             fetchUser(userId)
                 .then(setUser);
         }, [userId]);
     }
    

    After: we can directly pass the promise to the component and consume it using use

     //After
     function UserDetailComponent({ userPromise }) {
         const user = use(userPromise);
     }
    
     function UserProfileComponent({ userId }) {
         // this is just to ensure we did not create promise on every render.
         // usually we use some data fetching library which will return a same promise
         // accross re-renders.
         const userPromise = useMemo(() => {
             return fetchUser(userId);
         }, [userId]);
    
         return (
             <React.Suspense fallback={"Loading..."}>
                 <UserDetailComponent userPromise={userPromise} />
             </React.Suspense>
         );
     }
    

Enhancements

ref as component props:

Hereafter we can take ref from component props, instead of declaring it as new argument to component function.

// before
export React.forwardRef(function UserDetails(props, ref) => {
});

//after
export function UserDetails({ user, ref }) {
   useImperativeHandle(ref, function () {
    return {};
   });
}

//...
<UserDetails ref={ref}/>

ref callback returns the cleanup function:

With this enhancement we can remove unwanted boiler plate code

// before
function ProfileDetail() {
    const ref = React.createRef();

    useEffect(() => {
     const listener = () => {};
     ref.current.addEventListener("keydown", listener);
     return () => ref.current.removeEventListener("keydown");
    }, []);

    return (
        <input ref={ref}/>
    )
}

//after
function ProfileDetail() {
    function inputRef(ref) {
       const listener = () => {};
       ref.current.addEventListener("keydown", listener);
       return () => ref.current.removeEventListener("keydown");
    }

    return (
        <input ref={inputRef}/>
    )
}

Meta tags support in Components:

We can now change the document Title, based on where the user is in client components.

<Router>
    <Route path="/dashboard" element={Dashbaord} />
    <Route path="/profile" element={Profile} />
</Router>

function Dashboard() {
    return (
        <title>Sales Dashboard</title>
    )
}

function Profile() {
    return (
        <title>Johon Profile</title>
    )
}

Should you really care:

Provided all these new features and enhancements, what does it means if you user using data management library like React Query, SWR should you really care about all these new data management option react has brought built in.

IMO, these are all low-level hooks that library author can use to remove boiler plate code inside the codebase. Because we are already fetching the data at the component level using the above data management libraries along with features like local caching and effective invalidations.

We can utilise the enhancements like ref, use(Context), meta tags to remove boiler plate code make our code base cleaner.

💡
The main motive of moving towards native form elements for data management is to opt for progressive enhancement. In that perspective, we can incrementally change the code base, so that app will work with minimal JavaScript.

Here is the sample playground.