Written by Emmanuel Odioko✏️
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.
Why React 19
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.
React 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.
Automatic memoization
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()
hook
The 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
directives
If 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;
}
Actions
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
Hook
This 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.
Document metadata
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.
Minor updates and features
These are some of the other interesting updates we will get to enjoy in React 19.
Asset loading
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.
Eliminating 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.
Exploring React 19 hooks
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 fetching
As 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:
Using 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()
hook
Before 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);
Action use case
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()
hook
The 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
hook
We 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:
Conclusion
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.
Get set up with LogRocket's modern React error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Script Tag:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)