Hey, have you finally mastered some hooks in React? If yes, great, but I’m sorry — you’ll have to drop a few.
Yes, I know: React is fond of new hooks and ways of doing things with each version release, and I am not a fan of it.
But if it makes you feel any better, the few hooks you will be dropping in React 19 are actually a pain.
So, with that in mind, let’s go over the new features and improvements in React 19. We’ll play around with these new features and say our goodbyes to old hooks.
Change is a byproduct of progress and innovation, and as React developers, we’ve been forced to adapt to these improvements over the years. However, I am more impressed with React 19 than I have ever been with previous versions.
Before we dive into its usage, let’s talk about the major changes we’ll see in React 19. One impressive and major change is that React will now use a compiler.
We’ve seen top frameworks copy important changes from each other, and just like Svelte, React will include a compiler. This will compile React code into regular JavaScript which will in turn massively improve its performance.
The compiler will bring React up to speed and reduce unnecessary rerenders. The compiler is presently used in production on Instagram.
A lot of the features that follow are possible because of the compiler, and it will offer a lot of automation for things like lazy loading and code splitting, which means we don’t have to use React.lazy anymore.
Let’s talk about those changes the compiler will bring beyond performance improvements.
For those who do not know the meaning of memoization, this is simply the optimization of components to avoid unnecessary re-renders. Doing this, you will employ useMemo()
and useCallback()
hooks. From experience, this has been an annoying process, and I will say I am so glad this is in the past now.
With memorization being automated, I think the compiler will do a much better job because, in large applications, it gets more confusing to figure out where we could use useMemo()
.
So yes, I am glad this has been taken out of hands, and we can say our goodbyes to the useMemo()
and useCallback()
hooks.
Use()
hookThe use()
hook means we won’t have to use the useContext()
hook, and it could temporarily replace the useEffect()
hook in a few cases. This hook lets us read and asynchronously load a resource such as a promise or a context. This can also be used in fetching data in some cases and also in loops and conditionals, unlike other hooks.
use client
and use server
directivesIf you’re a huge fan of Next.js, you are probably familiar with use client
and use server
directives. If this is new to you, just keep in mind that the use client
directive tells Next.js that this component should run on the browser, while use server
tells Next that this code should run on the server.
These directives have been available in Canary for a while now, and now we can now use them in React. Doing so comes with benefits like enhanced SEO, faster load time, and easier data fetching with the server. A very basic implementation could look like this:
use client
:
'use client'; import React, { useState } from 'react'; const ClientComponent = () => { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default ClientComponent;
use server
:
'use server'; export async function fetchData() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); return data; }
If you have used Remix or Next, you may be familiar with the action API. Before React 19, you would need a submit handler on a form and make a request from a function. Now we can use the action attribute on a form to handle the submission.
It can be used for both server-side and client-side applications and also functions synchronously and asynchronously. With this, we may not be saying bye to old hooks, but we will have to welcome new ones like UseFormStatus()
and useActionState()
. We will dive more into them later.
Actions are automatically submitted within transition()
which will keep the current page interactive. While the action is processing, we can use async await in transitions, which will allow you to show appending UI with the isPending()
state of a transition.
useOptimistic
HookThis is another hook that is finally no longer experimental. It’s very user-friendly and is used to set a temporary optimistic update to a UI while we wait for the server to respond.
You know the scenario where you send a message on WhatsApp, but it hasn’t ticked twice due to network downtime? Yeah, this is exactly what the useOptimistic
Hook simplifies for you.
Using this hook in partnership with actions, you can optimistically set the state of the data on the client. You can find more details by reading useOptimistic Hook in React.
Do you have a favorite package for SEO and metadata for React? Well, they fall under the things we will have to leave behind, as React 19 has built-in support for metadata such as titles, descriptions, and keywords, and you can put them anywhere in the component including server-side and client-side code:
function BlogPost({ post }) { return ( <article> {/* Post Header */} <header> <h1>{post.title}</h1> <p> <strong>Author:</strong> <a href="https://twitter.com/joshcstory/" rel="author" target="_blank"> Josh </a> </p> </header> {/* Meta Tags for SEO */} <head> <title>{post.title}</title> <meta name="author" content="Josh" /> <meta name="keywords" content={post.keywords.join(", ")} /> </head> {/* Post Content */} <section> <p> {post.content || "Eee equals em-see-squared..."} </p> </section> </article> ); }
Each of these meta tags in React 19 will serve a specific purpose in providing information about the page to browsers, search engines, and other web services.
These are some of the other interesting updates we will get to enjoy in React 19.
You probably have moments where you reload a page and you get unstyled content, and in moments like these, you’re rightfully confused. Thankfully, this shouldn’t be an issue anymore, because in React 19, asset loading will integrate with suspense making sure high-resolution images are ready before display.
forwardRef
ref
will now be passed as a regular prop. Occasionally we use forwardRef
which lets your component expose a dom node to a parent component with a ref
; this is no longer needed as ref
will just be a regular prop.
Lastly, React will have better support for web components, which will help you build reusable components. So, let’s get into the practical use cases so we have a better idea of how exactly React 19 will help us build faster websites.
The first on our list will be Use()
. There are no particular reasons to why these hooks are explained in this order, I only tried to explain them in a way it will be undersood better. Going further we will see how they are used:
use()
hook for fetchingAs you can tell, I’m very funny, and you might wonder how I’m able to come up with so much humor. Let me show you how you can create a small joke application with use()
so you can be funny too. This app will fetch new jokes whenever we refresh the page.
First, we will use the useEffect()
hook, and afterwards I’ll show how short when using the use()
hook:
useEffect()
import { useEffect, useState } from "react"; const JokeItem = ({ joke }) => { return ( <div className="bg-gradient-to-br from-orange-100 to-blue-100 rounded-2xl shadow-lg p-8 transition-all duration-300"> <p className="text-xl text-gray-800 font-medium leading-relaxed"> {joke.value} </p> </div> ); }; const Joke = () => { const [joke, setJoke] = useState(null); const [loading, setLoading] = useState(true); const fetchJoke = async () => { setLoading(true); try { const res = await fetch("https://api.chucknorris.io/jokes/random"); const data = await res.json(); setJoke(data); } catch (error) { console.error("Failed to fetch joke:", error); } finally { setLoading(false); } }; useEffect(() => { fetchJoke(); }, []); const refreshPage = () => { window.location.reload(); }; return ( <div className="min-h-[300px] flex flex-col items-center justify-center p-6"> <div className="w-full max-w-2xl"> {loading ? ( <h2 className="text-2xl text-center font-bold mt-5">Loading...</h2> ) : ( <JokeItem joke={joke} /> )} <button onClick={refreshPage} className="mt-8 w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-bold py-3 px-6 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2" > Reload To Fetch New Joke </button> </div> </div> ); }; export default Joke;
In the code above, we used the useEffect
hook to fetch a random joke, so whenever we reload the page, we get a new one.
Using use()
, we can completely get rid of useEffect()
and the isloading
state, as we will be using a suspense boundary for the loading state. This is how our code will look:
import { use, Suspense } from "react"; const fetchData = async () => { const res = await fetch("https://api.chucknorris.io/jokes/random"); return res.json(); }; let jokePromise = fetchData(); const RandomJoke = () => { const joke = use(jokePromise); return ( <div className="bg-gradient-to-br from-orange-100 to-blue-100 rounded-2xl shadow-lg p-8 transition-all duration-300"> <p className="text-xl text-gray-800 font-medium leading-relaxed"> {joke.value} </p> </div> ); }; const Joke = () => { const refreshPage = () => { window.location.reload(); }; return ( <> <div className="min-h-[300px] flex flex-col items-center justify-center p-6"> <div className="w-full max-w-2xl"> <Suspense fallback={ <h2 className="text-2xl text-center font-bold mt-5"> Loading... </h2> } > <RandomJoke /> </Suspense> <button onClick={refreshPage} className="mt-8 w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-bold py-3 px-6 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2" > Refresh to Get New Joke </button> </div> </div> </> ); }; export default Joke;
We used the use()
hook to unwrap the promise returned by the fetchData
function and access the resolved joke data directly within the RandomJoke
component.
This is a more straightforward way of handling asynchronous data fetching in React, and this should be the result:
Hilarious, right?
use()
hook replacing useContext()
hookBefore use()
, if I wanted to implement a dark and light mode theme, I would use the useContext()
hook to manage and provide a theme state in the component.
I would also have to create a ThemeContext
which would hold the current theme and a toggle function, and then the app would be wrapped in a ThemeProvider
to be able to access the context in a component.
For example a component like ThemedCard
will call useContext(ThemeContext)
to first access them and adjust style based on a user’s interactions:
import { createContext, useState, useContext } 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 useContext hook const { theme, toggleTheme } = useContext(ThemeContext); return ( <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900"> <div className={`max-w-md mx-auto shadow-md rounded-lg p-6 transition-colors duration-200 ${ theme === "light" ? "bg-white text-gray-800" : "bg-gray-800 text-white" }`} > <h1 className="text-2xl font-bold mb-3">Saying Goodbye to UseContext()</h1> <p className={`${theme === "light" ? "text-gray-600" : "text-gray-300"}`} > The use() hook will enable us to say goodbye to the useContext() hook and could potentially replace the useEffect() hook in a few cases. This hook lets us read and asynchronously load a resource such as a promise or a context. This can also be used in fetching data in some cases and also in loops and conditionals, unlike other hooks. </p> {/* Toggle button */} <button onClick={toggleTheme} className={`mt-4 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-opacity-50 transition-colors duration-200 ${ theme === "light" ? "bg-gray-600 hover:bg-blue-600 text-white focus:ring-blue-500" : "bg-yellow-400 hover:bg-yellow-500 text-gray-900 focus:ring-yellow-500" }`} > {theme === "light" ? "Switch to Dark Mode" : "Switch to Light Mode"} </button> </div> </div> ); }; const Theme = () => { return ( <ThemeProvider> <ThemedCard /> </ThemeProvider> ); }; export default Theme;
With use()
, you will do the same thing but this time you will only replace UseContext()
with the use()
hook: \
// Replace useContext() hook import { createContext, useState, useContext } from "react"; // Access the theme context directly using the use() hook const { theme, toggleTheme } = use(ThemeContext);
Dark theme:
Light theme:
For the action, we will be creating a post as an example. We will have a post form that will enable us to submit a simple update about either a book we’ve read, how our vacation went, or, my personal favorite, a rant about bugs interfering with my work. Below is what we want to achieve using Actions:
Here is how Action was used to achieve this:
// PostForm component const PostForm = () => { const formAction = async (formData) => { const newPost = { title: formData.get("title"), body: formData.get("body"), }; console.log(newPost); }; return ( <form action={formAction} className="bg-white shadow-xl rounded-2xl px-8 pt-6 pb-8 mb-8 transition-all duration-300 hover:shadow-2xl" > <h2 className="text-3xl font-bold text-indigo-800 mb-6 text-center"> Create New Post </h2> <div className="mb-6"> <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="title" > Title </label> <input className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300" id="title" type="text" placeholder="Enter an engaging title" name="title" /> </div> <div className="mb-6"> <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="body" > Body </label> <textarea className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300" id="body" rows="5" placeholder="Share your thoughts..." name="body" ></textarea> </div> <div className="flex items-center justify-end"> <button className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center" type="submit" > <PlusIcon className="mr-2 h-5 w-5" /> Create Post </button> </div> </form> ); }; export default PostForm;
If you’ve ever used PHP, using action in the form is very similar, so we are kind of taking things back to our roots. 😀 We have a simple form, and we attach action to it. We can call it whatever we want, and in my case, it is formAction
.
We go ahead to create the formAction
function. Well since this is an action we will have access to formData
. We create an object called newPost
and set that to an object with a title and body which we do have access to with a get method. Now, if we console log this, newPost
, we should be able to get the imputed values which are the title and the post:
And that’s pretty much it! I didn’t have to create an onClick
and add an event handler
; I just added an action. Below is the rest of the code:
import { useState } from "react"; import { PlusIcon, SendIcon } from "lucide-react"; // PostItem component const PostItem = ({ post }) => { return ( <div className="bg-gradient-to-r from-purple-100 to-indigo-100 shadow-lg p-6 my-8 rounded-xl transition-all duration-300 hover:shadow-xl hover:scale-105"> <h2 className="text-2xl font-extrabold text-indigo-800 mb-3"> {post.title} </h2> <p className="text-gray-700 leading-relaxed">{post.body}</p> </div> ); }; // PostForm component const PostForm = ({ addPost }) => { const formAction = async (formData) => { const newPost = { title: formData.get("title"), body: formData.get("body"), }; addPost(newPost); }; return ( <form action={formAction} className="bg-white shadow-xl rounded-2xl px-8 pt-6 pb-8 mb-8 transition-all duration-300 hover:shadow-2xl" > <h2 className="text-3xl font-bold text-indigo-800 mb-6 text-center"> Create New Post </h2> <div className="mb-6"> <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="title" > Title </label> <input className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300" id="title" type="text" placeholder="Enter an engaging title" name="title" /> </div> <div className="mb-6"> <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="body" > Body </label> <textarea className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300" id="body" rows="5" placeholder="Share your thoughts..." name="body" ></textarea> </div> <div className="flex items-center justify-end"> <button className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center" type="submit" > <PlusIcon className="mr-2 h-5 w-5" /> Create Post </button> </div> </form> ); }; // Posts component const Posts = () => { const [posts, setPosts] = useState([]); const addPost = (newPost) => { setPosts((posts) => [...posts, newPost]); }; return ( <div className="container mx-auto px-4 py-8 max-w-4xl"> <h1 className="text-4xl font-extrabold text-center text-indigo-900 mb-12"> Logrocket Blog </h1> <PostForm addPost={addPost} /> {posts.length > 0 ? ( posts.map((post, index) => <PostItem key={index} post={post} />) ) : ( <div className="text-center text-gray-500 mt-12"> <p className="text-xl font-semibold mb-4">No posts yet</p> <p>Be the first to create a post!</p> </div> )} </div> ); }; export default Posts;
useFormStatus()
hookThe above form works, but we can go a step further with the useFormStatus
. This way, we can have our submit button say disabled or do whatever we want while the form is actually submitting.
Two things to keep in mind; The first is that this hook only returns status information for a parent form and not for any form rendered in the same component. The second thing to note is that this hook is imported from React-Dom and not React.
In our form above we will pull the button into a separate component called SubmitFormButton()
, and what we will do is get the pending state, from useFormStatus
which will be true or false. Then we write our logic while it’s pending.
Our logic could be something as easy as saying, “If pending, display Creating post, else display Create post” and we can add a little delay so we see our changes. Let’s see how it looks in our code.
Submit component:
// SubmitButton component const SubmitButton = () => { const { pending } = useFormStatus(); console.log(pending); return ( <button className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center" type="submit" disabled={pending} > <PlusIcon className="mr-2 h-5 w-5" /> {pending ? "Creating Post..." : "Create Post"} </button> ); };
Stimulating a delay in our form submission:
// PostForm component const PostForm = ({ addPost }) => { const formAction = async (formData) => { // Simulate a delay of 3 seconds await new Promise((resolve) => setTimeout(resolve, 3000)); const newPost = { title: formData.get("title"), body: formData.get("body"), }; addPost(newPost); };
We go ahead and render the PostForm
component, and we should have this 👍
The button is also disabled at the same time so you can’t keep clicking it until the post is created.
useActionState
hookWe can refactor our code using the useActionState()
hook. What the useActionState
hook does is combine the form submission logic, state management, and loading state into one unit.
Doing so automatically handles the pending state during a form submission, allowing us to easily disable the submit button like the useFormStatus
hook, show a loading message, and display either a success or error message.
Unlike the useFormStatus
, the useActionState
will be imported from React, and this is how it is used: \
const [state, formAction, isPending] = useActionState( async (prevState, formData) => { // Simulate a delay of 3 seconds await new Promise((resolve) => setTimeout(resolve, 3000)); const title = formData.get("title"); const body = formData.get("body"); if (!title || !body) { return { success: false, message: "Please fill in all fields." }; } const newPost = { title, body }; addPost(newPost); return { success: true, message: "Post created successfully!" }; } );
In the code above useActionState
is used to handle our submission where it extracts the title and body using the formData
API, and then it validates the inputs and returns a success and error state. This is how it looks:
The major thing React 19 offers is helping developers build a lot faster websites, and I am glad to have been able to play my part in introducing this to you. Feel free to ask questions below and also give your two cents on this new version.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowChartDB is a powerful tool designed to simplify and enhance the process of visualizing complex databases. Explore how to get started with ChartDB to enhance your data storytelling.
Learn how to use JavaScript scroll snap events for dynamic scroll-triggered animations, enhancing user experience seamlessly.
A comprehensive guide to deep linking in React Native for iOS 14+ and Android 11.x, including a step-by-step tutorial.
Create a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.