React 19: All You Need to Get Started Using it

React 19: All You Need to Get Started Using it

Regarding JS libraries and frameworks, React stands out due to the large community of developers that have adopted it as their preferred UI library and its large ecosystem of third-party packages that can make development easier and faster.

As always, React tends to release newer versions with new and improved features in an attempt to fix previous bugs. On April 25, 2024, React 19 was released, and it served as another stepping stone in the development of the React ecosystem, providing a variety of new features and updates to help make development less tedious. In this article, we will go over the new features that React 19 has to offer React developers.

Pre-requisites:

  1. knowledge of ReactJS, Tailwind CSS

  2. knowledge of HTML, CSS, and JavaScript

Key Features of React 19

Let’s delve straight into the topic. The most important features of React 19 to be discussed extensively are as follows:

1. React Compiler

React 19 offers a compiler similar to other frameworks, such as Angular and Svelte. This compiler basically converts React code into regular JS code. This will help to analyze React code at build time and automatically optimize it for better performance, especially when dealing with updates and re-renders. Most of the new features in React 19 are directly related to the addition of the compiler.

The compiler replaces the need for us to write code using hooks like useMemo, and useCallback to optimize components. These hooks are currently used to manually control how React re-renders parts of your UI.

Let’s take a look at a code example where manual memoization is needed before looking at one involving a compiler:

import { useState, useCallback, useMemo } from 'react'

const Increment = () => {
    const [count, setCount] = useState(0);
    const increment = useCallback(() => setCount((c) => c + 1), []);

    const double = useMemo(() => count * 2, [count]);


    return(
        <>
            <div>count: {count}</div>
            <div>doubleCount: {double}</div>
            <button onClick={increment}>Increment</button>
        </>
    )
}



export default Increment;

In this code, the Increment component is a simple counter that displays the current count and its double. The count state is used to store the current value of the counter. The increment function is used to increase the count by 1. If we use useCallback, it ensures that the function is created only once, improving performance. The double value is calculated based on the count and is memoized using useMemo to avoid unnecessary recalculations. The increment component renders the count, doubleCount, and a button to increment the count.

Now, let’s see how React Compiler removes the use of usecallbackand useMemo hooks:

import { useState } from 'react'

const Increment = () => {
    const [count, setCount] = useState(0);
    const increment = () => setCount((c) => c + 1);

    const double = count * 2;


    return(
        <>
            <div>count: {count}</div>
            <div>doubleCount: {double}</div>
            <button onClick={increment}>Increment</button>
        </>
    )
}



export default Increment;

Note that we only imported the useState hook. In the Increment component, useState(0) initializes a state variable count to 0. SetCount changes the count state. The increment function modifies the count state by adding 1 to the current value of setCount. The double variable returns a calculated value based on the current count state.

2. Server components

Server components are a new way to render React components ahead of time on the server. By default, React components render on the client side in the user's browser. This is a feature that already exists in next.js, which is a framework built on React, where all components are server components. These server components will render on the server and generate HTML that gets sent to the client. The code will not be included in the JavaScript bundle sent to the browser, resulting in faster initial load times.

To allow React code to run on the server, add a directive at the top of our component file. We must write 'use server' on the component's first line to convert it into a server component. Server components provide the following advantages:

  • Improved SEO: Search engines may simply scan and index the content of server-rendered components, resulting in improved Search Engine Optimization (SEO). This is because the original HTML delivered to the client already has the content, making it easier for search engines to understand.

  • Faster initial load times: Since server components are rendered on the server, the browser receives the first HTML content much more quickly. This results in a loading time perceived as fast for users, particularly on slower connections.

  • Enhanced Performance: By shifting rendering work to the server, server components can boost our application's overall performance. This is because the client-side JavaScript bundle is smaller, resulting in speedier download and execution times.

Let’s take a look at how to apply the ‘use server’ in a component:

// serverComponent.js

'use server'; 
async function Directive() {
  const response = await fetch('https://api.quotable.io/random');
  const data = await response.json();
  const quote = data.content;
  const author = data.author;

  return (
    <div>
      <p>{quote}</p>
      <p>- {author}</p>
    </div>
  );
}

export default Directive;

The line 'use server' turns the component into a server component. Directive is defined as an asynchronous functional component that uses async. This enables the component to use await to wait for network requests.

const response = await fetch('https://api.quotable.io/random') gets information from the API endpoint for a random quote. await stops the function's execution until the promise returned by fetch is resolved, at which point the response is obtained. const data = await response.json(); parses the response body into JSON. const quote = data.content, and const author = data.author extracts the quote and author information, respectively, from the parsed JSON object.

3. Actions

The Actions feature was created to make form handling easier. They are basically functions that are called when a form is being submitted. It separates form submission logic from the component itself, and this makes the code cleaner. Instead of using the onSubmit event, we can use the action attribute. Just like the onSubmit event, this takes a function that allows us access to the submitted form data for easy form handling.

Actions can be used on both the client-side and server-side, and this means we can execute both synchronous and asynchronous operations with actions.

Let’s take a look at an example where the action attribute is being used on the server-side:

'use server'
const formAction = (formData) => {
    const newPost = {
        Title: formData.get('Title'),
        Body: formData.get('Body')
    }
    console.log(newPost)
}

const Form = () => {
    return <form action={formAction}>
        <div>
            <label>Title</label>
            <input type="text" name='Title'/>
        </div>
        <div>
            <label>Body</label>
            <input type="text" name="Body" />
        </div>
        <button type='submit'>Submit</button>
    </form>
}

export default Form;

The line 'use server' is used to mark the Form component as a server component and it returns a form element. The action attribute of the form is set to the formAction function. The form contains two input fields, Title and Body. A submit button is also included.

formAction refers to the action function replacing the onSubmit event, and it takes formData as an argument. We extract the Title and Body values from the form data and create a newPost object. We log out newPost to the console.

4. use() API

The use() API is a new feature in React 19, and it basically allows us to read and asynchronously load a promise or a context. It is quite versatile and can be used for fetching data instead of using the useEffect hook. It is going to replace the useContext hook because we can simply pass in a context as a parameter to the use() API (use(context)).

Let’s take a look at how the use() API hook will replace useEffect when it comes to fetching data:

Fetching data with useEffect Hook

import { useState, useEffect } from 'react';

const UserItems = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
          <p>{user.phone}</p>
        </div>
      ))}
    </ul>
  );
};

const Users = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/users');
        const data = await res.json();
        setUsers(data);
        setLoading(false);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  return <UserItems users={users} />;
};
export default Users;

In this code, we use useState to create two state variables,users and loading within the Users component. Users is an array to store user data, initially empty ([]). Loading is a boolean indicating whether data is being fetched (initially true). In this Users component, we also use the useEffect hook with an empty dependency array ([]) to run the effect code only once after the component mounts.

Inside the useEffect hook, an async function fetchUsers is defined to fetch data from an API. The function fetches data from https://jsonplaceholder.typicode.com/users using fetch and parses the response as JSON. On successful fetch, it updates the users state with the fetched data and sets loading to false. In the case of an error, it logs the error to the console. Finally, it sets loading to false to indicate the loading state regardless of success or failure.

The Users component conditionally renders content based on the loading state. If loading is true, it displays a "Loading..." message. If loading is false (data is fetched), it renders the UserItems component, passing the fetched users data as a prop.

The UserItems component takes a users prop containing an array of user objects. We map over the users array and render a list item for each user. Each list item displays the user's name in a <h2> tag and their phone number in a <p> tag. The key prop on the div is set to the user's id to improve performance.

Fetching Data With Use() API

import { use, Suspense } from 'react';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};

const UserItems = () => {
  const users = use(fetchUsers());

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
          <p>{user.phone}</p>
        </div>
      ))}
    </ul>
  );
};

const Users = () => {
  return (
    <Suspense
      fallback={
        <h1>Loading...</h1>
      }
    >
      <PostItems />
    </Suspense>
  );
};

export default Users;

Let’s take a look at how the use() API will replace useContext hook:

import React, { createContext, useState, use } from 'react';

// Create a context object
const ThemeContext = createContext();

// Create a provider component
const ThemeProvider = ({ children }) => {
  // State to hold the current theme
  const [theme, setTheme] = useState('light');

  // Function to toggle theme
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    // Provide the theme and toggleTheme function to the children
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemedCard = () => {
  // Access the theme context using the use() hook
  const { theme, toggleTheme } = use(ThemeContext);

  return (
    <div
      className={`max-w-md mx-auto shadow-md rounded-lg p-6 ${
        theme === 'light' ? 'bg-white' : 'bg-gray-800'
      }`}
    >
      <h1
        className={`text-2xl mb-3 ${
          theme === 'light' ? 'text-gray-800' : 'text-white'
        }`}
      >
        Themed Card
      </h1>
      <p className={theme === 'light' ? 'text-gray-800' : 'text-white'}>
This card magically changes color. Don’t ask me how.
      </p>
      {/* Toggle button */}
      <button
        onClick={toggleTheme}
        className='mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-600'
      >
        {theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
      </button>
    </div>
  );
};

const Theme = () => {
  return (
    <ThemeProvider>
      <ThemedCard />
    </ThemeProvider>
  );
};

export default Theme;

In this example, we use createContext to create a new context object named ThemeContext.

ThemeProvider is the component that manages the theme state. ThemeProvider uses the useState hook to manage the theme state, and it also provides the theme and toggleTheme functions to its children using ThemeContext.Provider.

We use the ThemedCard component to access the theme context using useContext(ThemeContext) and extract theme and toggleTheme from the context. It conditionally applies styles based on the theme value, and there is a button within it for toggling the theme.

We use the Theme component to wrap ThemedCard with ThemeProvider to make the theme context available to ThemedCard.

5. The useFormStatus() hook

The useFormStatus hook is a new React hook that provides information about the status of the last form submission. It's useful for tracking the status of form submissions and delivering feedback to users.

It returns a status object, which contains information about the form's current state. It also provides us with a pending property to indicate whether the form is currently submitting. The useFormStatus hook also returns data after a successful form submission or errors encountered during form submission.

Let’s take a look at a use case of the useFormStatus hook:

import { useFormStatus } from 'react-dom'

const SubmitButton = () => {
    const { pending } = useFormStatus();
    console.log(pending);

    return (
      <button type='submit' disabled={pending}>{pending ? 'Submitting...' : 'Submit'}
      </button>
    );
  };

const formAction = async (formData) => {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    const newPost = {
        Title: formData.get('Title'),
        Body: formData.get('Body')
    }
    console.log(newPost)
}

const Form = () => {
    return <form action={formAction}>
        <div>
            <label>Title</label>
            <input type="text" name='Title'/>
        </div>
        <div>
            <label>Body</label>
            <input type="text" name="Body" />
        </div>
        <div>
        <SubmitButton />
        </div>
    </form>
}

export default Form;

In this code, we import the useFormStatus hook from the react-dom package. const { pending } = useFormStatus(); destructures the pending property from the hook's return value. The pending property is a boolean indicating whether the form is currently being submitted.

The SubmitButton component renders a submit button and uses the pending value to conditionally disable the button and change its text based on the status of our submission.

Inside the Form component, we have two input fields for Title and Body. The SubmitButton component is nested within the form. The action attribute of the form is set to formAction.

formAction takes a new Promise that resolves after 2 seconds during form submission. It creates a newPost object from the form data. We only log the newPost object onto the console, but normally we would send the form data to a server.

6. The useActionState() hook

The useActionState hook uses action functions that handle form submission logic and an initial state for the form. This initial state is then updated accordingly, depending on the outcome of a form submission. In a way, it’s similar to the useState hook, but it only works for actions.

The syntax is as follows:

const [state, formAction] = useActionState(fn, initialState, permalink);

The parameters are as follows:

1. fn: The fn function is called after a form is submitted or a button is clicked. When invoked, it receives the form's previous state value as its first argument and the submitted form data.

2. initialState: The initial value we want the state to be.

3. permalink: This is negligible. If fn is an action that will run on the server and the form is submitted before the JavaScript bundle loads, the browser will navigate to the specified permalink URL.

The useActionState hook returns the following:

1. state: Before an action is invoked, the current state matches the initialState we have passed. Once the action is invoked, it will match the value returned by the action.

2. formAction: It returns a new action that we can pass as action prop to our form component or formAction prop to any button component within the form.

Let’s take a look at an example:

import { useFormState} from 'react-dom';

const ActionState = () => {
    const submitForm = (prevState, formData) => {
        const name =  formData.get("username");
        if(name === 'john' || "jack"){
            return {
                success: true,
                text: `Welcome ${name}`
            }
        }
        else{
            return {
                success: false,
                text: "Error"
            }
        }
    }
    const [ message, formAction ] = useFormState(submitForm, null)
    return <form action={formAction}>
        <label>Name</label>
        <input type="text" name="username" />
        <button>Submit</button>
        {message && <h1>{message.text}</h1>}
    </form>
}

export default ActionState;

In this code, we import the useFormState hook from the react-dom package. const [ message, formAction ] = useFormState(submitForm, null) defines how we apply the useFormState hook, and it does the following:

  • It creates a state variable message to store the form submission result (success or error).

  • It creates formAction to handle the form submission.

  • The submitForm function is passed as the first argument to useFormState to define the form submission logic.

  • The second argument, null, is the initial form data. In this example, there is no initial data.

We pass the submitForm function as the first argument to the useFormState hook. By default, submitForm is expected to take prevState (previous form state) and formData as parameters. However, we don’t need prevState in this example. The submitForm function basically extracts the username from the form data and checks if the username is either "john" or "jack". If the username matches, it returns a success message with the username. Otherwise, it returns an error message.

We used the ActionState component to create a form with a name input field and a submit button. The action attribute of the form is set to the formAction function. Inside the ActionState component, a success or error message is displayed based on the message state.

7. The useOptimistic() hook

The useOptimistic hook is a React hook that allows us to implement optimistic updates in our applications. This means we can update the UI immediately after a user action, even before the server confirms the operation. This creates a more responsive and fluid user experience. When a user submits a form or sends a message, instead of waiting for the server’s response to reflect the changes, the interface is immediately updated with the expected outcome in an optimistic manner. We would see our form data or message along with a pending property such as a “sending…” flag.

The syntax is as follows:

const [ optimisticState, addOptimisticState] = useOptimistic(state, updatefn);

The parameters are as follows:

1. State: This is the value to be returned initially and anytime no action is pending.

2. updateFn(currentState, optimisticValue): This function accepts the current state and the optimistic value passed to addOptimistic and returns the optimistic state.

The useOptimistic hook returns the following;

1. optimisticState: This is the optimistic state. It is equal to the initial state value unless an action is in progress, in which case it is the value returned by updateFn.

2. addOptimisticState: This refers to the function to call when we have an optimistic update. It will call the updateFn with state and optimisticValue.

Let’s take a look at an example where the useOptimistic hook is applied:

import { useOptimistic, useState, useRef } from 'react';

const MessageForm = ({ addOptimisticMessage, sendMessage }) => {
  // Create a reference to the form
  const formRef = useRef();

  // This function is called when the form is submitted
  const formAction = async (formData) => {
    addOptimisticMessage(formData.get('message'));

    // Clear the form
    formRef.current.reset();

    await sendMessage(formData);
  };

  return (
    <form action={formAction} ref={formRef} className='flex items-center mb-5'>
      <input
        type='text'
        name='message'
        placeholder='Hello!'
      />
      <button
        type='submit'
      >
        Send
      </button>
    </form>
  );
};

const Thread = ({ messages, sendMessage }) => {
  // The useOptimistic hook is used to add an optimistic message to the list of messages
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true,
      },
    ]
  );

  return (
    <div>
      <MessageForm
        addOptimisticMessage={addOptimisticMessage}
        sendMessage={sendMessage}
      />
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          <span>{message.text}</span>
          {message.sending && (
            <small>(Sending...)</small>
          )}
        </div>
      ))}
    </div>
  );
};

const deliverMessage = async (message) => {
  // Simulate a delay
  await new Promise((res) => setTimeout(res, 1000));
  return message;
};

const Message = () => {
  const [messages, setMessages] = useState([]);

  async function sendMessage(formData) {
    const sentMessage = await deliverMessage(formData.get('message'));

    setMessages((messages) => [...messages, { text: sentMessage }]);
  }

  return <Thread messages={messages} sendMessage={sendMessage} />;
};

export default Message;

In this code, we import useOptimistic, useState, and useEffect from the react package. We use the useOptimistic hook in the Thread component to manage the displayed messages, and it takes two arguments, messages, and an updater function. messages (initial state) is an array of existing messages. The updater function receives the current state and the new message to add. useOptimistic returns the updated state with the new message and a "sending" flag.

The messageForm component handles user input and message submission, and inside it, we use formRef to access the form element. Within the messageForm component, we define formAction which calls addOptimisticMessage with the submitted message to add it optimistically to the displayed list, then clears the form after submission, and eventually calls sendMessage to send the message to the server.

The Thread component renders the message form and the message list and uses useOptimistic to manage the displayed messages, including optimistic updates. Within the Thread component, we map over optimisticMessages to display each message with its text and a "Sending..." indicator if the sending flag is true.

We use the deliverMessage function to simulate a delay of 1 second for the message delivery, and it returns the message for updating the state after the delay.

The Message Component is our main component, and it manages the overall messages state using useState. Inside this component, we define sendMessage, which calls deliverMessage to send the message and update the state with the sent message after the delay. The Thread component is nested within the Message component, which renders it, passing the current message state and the sendMessage function.

8. Document Metadata

React 19 will provide built-in support for managing document metadata, which includes elements like titles, descriptions, and meta tags. These elements are necessary for the SEO optimization of websites. In the past, we had to use third-party libraries such as react-helmet for handling document metadata. With this latest version of React, we no longer require the use of external libraries.

We can now put the title and meta tags anywhere within our component. React 19 automatically hoists the defined <title>, <link>, and <meta> tags to the <head> section of the document. This ensures proper placement and functionality regardless of rendering methods whether client-side or server-side.

const Home = () => {
  return (
    <>
      <title>Yangy’s World</title>
      <meta name="description" content="Freecode camp blogs" />
      <h1>Hello World</h1>
    </>
  );
}

export default Home;

This code shows a React component , Home, that includes document metadata elements <title> and <meta>. The <title> tag gives the webpage the title, "Yangy’s World." The <meta> tag creates a meta description for the webpage, which is a brief summary of the page's content.

9. Ref as a Prop

In React 19, we can now pass ref directly as a prop to a functional component when using the useRef hook. In times past, to pass a ref to a child component, we had to first create the ref and then pass the ref as a prop to the child component, and then it could be accessed within the child component using forwardRef. React 19 eliminates the need for forwardRef and the example below will illustrate this major change:

Using forwardRef With the useRef Hook:

import React, { useRef, forwardRef } from 'react';

const Ref = () => {
  const inputRef = useRef(null);


  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

  const InputWithRef = forwardRef((props, ref) => {
    return <input type="text" ref={ref} />;
  });

  return (
    <div>
      <InputWithRef ref={inputRef} />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
};

export default Ref;

In this code, we import useRef and forwardRef from the react package. We use the useRef hook to create a ref object, inputRef, that can be attached to a DOM element. inputRef.current holds a reference to the DOM element after it's mounted.

The focusInput function defines a function to focus the input element and checks if inputRef.current is not null (meaning the input element exists). If it exists, it calls the focus() method on the element to set focus.

The Ref component nests the InputWithRef component and passes the inputRef to it. It contains a button that calls the focusInput function when clicked.

The InputWithRef component uses forwardRef to receive a ref as a prop. The received ref is then passed to the input element.

Now with React 19, we only have to make a minor alteration to the InputWithRef component by passing ref directly as a prop.

const InputWithRef = ({ props, ref }) => {
  return <input type="text" ref={ref} />;
};

Conclusion

We have been able to cover React’s major features, and hopefully, we will all learn to use their full potential. The React compiler is a game-changer because it will handle performance optimization automatically, allowing developers to focus more on building features and less on performance. React 19 is a significant step forward for the React ecosystem. Once a developer can understand these new features, he/she will be able to create more performant, scalable, and user-friendly applications.

References

  1. Neha, S. (2024, March 27). New Features in React 19 – Updates with Code Examples. FreeCodeCamp. https://www.freecodecamp.org/news/new-react-19-features/

  2. Brad, T. [Traversy Media]. (Mar 6, 2024). Exploring React 19 Features. Youtube.

  3. React Team. (2024, April 25). React 19 RC. Meta. react.dev/blog/2024/04/25/react-19