Home >Web Front-end >JS Tutorial >React Update Notes Companion
Update notes for new major versions of React are always incredibly information dense, and the framework itself is so broad that I always find myself taking notes and comparing vs reference documentation to try to not just figure out what changed and how to use it, but also how the various major concepts the update notes talk about actually worked under the hood. While React is extremely well documented, the update notes aren’t always “enough” to understand the concepts - for example, in the React 19 update notes, I found useOptimistic’s example confusing and actually a little inaccurate to how the hook actually works.
I’ve decided to write the notes I took while going over the React 19 changes into a companion for anyone who’s been dreading combing through the notes and figuring out what they all mean - so let’s get started.
Actions & Form Handling/useActionState
The first major improvement has been huge improvements to form handling. React has gradually been moving to remove boilerplate and better integrate with native HTML forms, and its new useActionState is part of this push. It’s intended to help improve error handling (as it’s now built-in), it automatically tracks loading states so that you don’t have to manually code in pending states constantly, and improves support for progressive enhancement.
Now, I found the code example provided for useActionState to be pretty confusing, and in fact this code example is the entire reason I started writing this article - there are, in fact, two parts to useActionState, which they combined in the example to save space, but ended up greatly reducing clarity. useActionState returns a tuple that tracks when the async action in the form submission is complete, while a modified version of the action you pass it is directly passed to the form. useActionState itself takes two inputs - an async formAction (which receives both the previous state and the form data as arguments when called) and the initial state - which can be null, zero, or a variable.
If your form hasn’t yet been submitted, the previous state is the initialState. If the form has been previously submitted, it’s whatever was submitted - in a server function, you can actually display the server response before hydration even happens. useActionState lastly can accept a string with a unique page url that the form is intended to modify. In other words, it can also accept an optional permalink parameter, which is particularly useful for progressive enhancement - it tells the browser where to navigate if a user submits the form before JavaScript has loaded.
Lastly, the tuple that useActionState returns is an array consisting of the current state (during your initial render it’s your initialState value), the react-modified formAction you can pass to a form as an action or a button as a prop, and an isPending flag/stateful variable. I’m looking forward to seeing what other new developments the React team will come up with, since this one seems particularly useful.
React-DOM updates
This react-dom addition will be familiar to anyone who has been using NextJS and form actions, and it seems the Reac team has decided form actions are ready for prime-time. For anyone who hasn’t used them in NextJS or another framework on top of react, they are basically a way to enhance React’s performance by using native form submission. Instead of onClick, you can pass native form submissions via the action prop - any function passed to action or formAction will have its submission handled automatically. React will also automatically reset any uncontrolled form fields. You also have manual options for resetting it via API. The React team has also integrated error handling with error boundaries. I won’t talk too much about it since I’m assuming most people remember them from NextJS, but I can write a followup if anyone has questions.
useFormStatus hook
This is a great addition to help you see what’s going on in your form without prop drilling or using context - if you’re asking why not prop drill, it has to do with keeping code maintainable and easy to modify. As for context, overusing context is going to cause performance issues since every component subscribed to a particular context will rerender whenever something in the context changes. So it declutters code, reduces the chance of errors, and stops you from gumming up your app’s performance.
The hook returns an object with four properties: pending - a boolean that says if there’s a pending submission, data - a formData object with the data submitted by the parent form (this is null if there is no active submission or parent form), method (get or post), and action - which is the action being passed through the action prop.
useOptimistic hook
The new simpler way to manage optimistic updates. If you’re not familiar with optimistic updates, it means updating the client-side display before the server-side updates happen. If you’ve ever liked something, seen the animation play and had it register on your screen as liked, then received a toast error saying “like failed”, this is due to optimistic rendering.
The useOptimistic hook accepts a stateful variable you want optimistically rendered and an update function, which must be a pure function - in other words, a deterministic function with no side effects. Basically, the update function retrieves the source of the update to state - so typically something like formData.get(‘name’). useOptimistic then returns two values: the optimistic state and an addOptimistic function.
I found the documentation for this a little weak, particularly around the usage flow - basically, you call useOptimistic and pass it the initial state that you want to display optimistic updates for and an update function. You receive two functions - the newly optimistic-enhanced stateful value (optimisticState) and a function to optimistically change state. When you have a new value submitted by the user, you call the second function, referred to as addOptimistic in the docs, and pass it the user-submitted value. In your component, you then pass it the optimistic-enhanced stateful value whenever you want to render the stateful var optimistically.
Overall, I really like this more standardized way of performing optimistic updates - I’ve previously had issues with caching in NextJS and making optimistic updates, so a standardized way of creating optimistic updates is great, and I’m sure this will bubble up to NextJS, if it hasn’t already.
The use API
This is a super dense API, and it’s a brand new way to access resources while react is rendering a page - the exact phrasing used is “reading resources in render”. So what specifically is it for? This new API can be used to access component information inside of conditionals or loops. If you aren’t familiar with why this is useful, it has to do with how React’s rendering process works. React/react-fiber relies on rendering everything in the same order each time, which is why you can’t generally access most hooks during the rendering process. To put it more clearly, state is actually tracked based on the order hooks are called in, so if hooks are called in an unpredictable order, you end up with rendering bugs. A great example is accessing a theme context depending on whether or not the user is logged in.
So why is this an important development? It means that you can load information/data only when it’s actually necessary, e.g. only if a user is actually logged in will you load css for a special theme. The data has to be serializable, so that means you can send a promise from the server component to a client component while it is in fact already in flight - this means there are fewer waterfall requests and they are automatically put in parallel. It's worth noting that when working in Server Components, you should actually prefer using async/await over use for data fetching - this is because async/await will resume rendering from exactly where it left off, while use will trigger a full re-render of the component after the data resolves. Of course, I also want to note that this change actually also means you have a new potential source of waterfall requests if you configure use incorrectly.
One really important thing to note is that you cannot use the “use” API in a try/catch block - this is because “use” automatically uses react suspense boundaries when called with a promise. A try/catch block prevents you from ever reaching the react level since any error would actually stop execution on the JS level before you ever reach React, breaking the functionality. Like other hooks, they have to be in the top level of scope of a particular component or function (again, due to render order).
So, what is the actual purpose of “use” is to help you access context to render things conditionally and only fetch data when it’s actually necessary. It’s yet another step towards making react a bit less arcane, simplifying conditional data fetching, improving dev experience, and improving performance all in one fell swoop. It requires more experienced React devs to relearn how to do things like theming, but I think it makes the framework a lot more accessible to new users, which is always great.
New React Dom Static APIs
These two new static APIs, prerender and prerenderToNodeStream are both improvements to renderToString, which is used for Server Side Rendering (SSR). These new improvements are for doing Static Site Generation (SSG) using renderToString. Unlike traditional streaming SSR which can send content chunks as they become available, these new APIs specifically wait for ALL data to load before generating the final HTML. They're designed to work seamlessly with modern streaming environments like Node.js Streams and Web Streams, but they intentionally don't support streaming partial content - instead, they ensure you get complete, fully-loaded pages at build time. They differ from traditional streaming SSR methods, which send pre-rendered sites as data becomes available.
We already had SSG-capable frameworks built on top of React, like NextJS, but this is intended to be React’s native functionality for SSG. The frameworks that had SSG used renderToString, then built their own complex data fetching coordinations around it. This was extremely difficult for users to create themselves, and these frameworks used extremely complex pipelines to do so. What these new methods do is essentially allow for data to load during static HTML generation, If you aren’t familiar with SSG, it’s essentially rendering everything at build time, and is undoubtedly the fastest method of rendering pages, as it doesn’t have to render pages on user request, so it’s great to have this kind of functionality for people who don’t want to use something like Next, which either requires deployment on Vercel or an extremely complex deployment process.
Server Components
The concept of React Server Components won’t be new to anyone who’s used a recent version of NextJS, but for anyone who hasn’t, server components are a new paradigm around data fetching. Frankly, the concept of Server Components (and Server Actions) deserve an entire article themselves, but to sum up the concept briefly, server components are always rendered on the server before being sent to the client, even after javascript is loaded. That means subsequent renders are done server-side.
The main advantage of these components is security and data fetching: if you request data inside a server component, the request information never shows up client-side, only the response, making it much more secure. APIs, endpoints, etc. simply are not accessible client-side. It also reduces bundle size since the javascript for said actions is never sent to the client. It additionally allows you to execute memory or computationally intensive operations on the server to reduce the burden of rendering on less powerful machines. They also reduce client-side waterfalls since sequential data fetching can be performed on machines closer to your databases, but of course it also opens up the possibility of brand new server-side waterfalls since your devs lose access to easy request information from test browser developer tools and have to use something like an OpenTelemetry collector and viewer to examine them. Lastly, server components are also great for progressive enhancement.
Server components also come with a list of limitations: you can’t use local browser APIs (local storage, window, etc), react hooks will work differently, you can’t rely on or use state, and you can’t use event handlers, so user interactivity for the components is decidedly slim. Basically, think of this paradigm as data fetching in server components, interactivity on client components.
The most important caveat for anyone new to server components is that you cannot import them into a client component - if you do, this will error out (assuming you’ve added some data fetching) because it causes the compiler to treat said server component as a client component. If you want to pass a server component to a client component, you need to pass it via the {children} prop.
Server Actions
These are another complex topic with a lot of implications for how you build your products and features, and these have also been present in NextJS for a year or so. Server actions are declared by typing ‘use server’ at the top of a file, and pass a direct reference to said server function which can then be called from inside of a client component.
On some level, these are conceptually similar to Remote Procedure Calls (RPC) - they both let you call server-side functions from the client, both abstract the complexity of client-server interactions, and both handle serialization and deserialization, but there are a few key differences to be aware of. The main benefit of server actions is that they work with React’s progressive enhancement,, help enforce type-safety across client/server boundaries, have built-in performance optimizations (related to progressive enhancement), and have a more seamless integration with the React ecosystem in that they provide built-in pending states and are automatically integrated with native form submissions.
When you’re talking about modern RPC, something like gRPC that already has type safety and some performance optimizations, the main advantage of server actions generally boils down to said built-in form handling and progressive enhancement, but importantly it also works with react suspense and error boundaries. Most importantly, deployment is a lot simpler since you don’t necessarily need to set up separate server for gRPC, so these are absolutely more ideal for smaller projects, though when it comes to larger projects I can see gRPC being a lot more desirable since it gives you more flexibility in terms of backend language, etc.
Context as a Provider
This is essentially a simplification of the syntax, helping React in general have a much cleaner, declarative syntax. To be honest I don’t have much to say about this aside from “I like it”.
Ref as a Prop & Cleanup Functions for Refs
Previously, to clean up refs, you had to perform a null check inside of your component, and the ref would be cleaned up at a somewhat indeterminate time. React would call your ref callback with null when the component unmounted, requiring you to handle both the attachment and detachment cases explicitly. The advantage of the new ref syntax is deterministic cleanup - which means it’s now a lot easier to work with external resources and third party libraries because you will know exactly when ref cleanup happens (on unmount). With the new approach, you can return a cleanup function directly. The TypeScript integration requires explicit return statements to avoid ambiguity about cleanup functions.
I really like the way this was implemented since it’s the same pattern from useEffect - maintaining consistency across the framework is great. I think this new ref cleanup is specifically going to be really useful for WebGL contexts and other heavy resources, but also for handling DOM event listeners added using native JS methods. This is because previously, react would remove the ref to the dom element on cleanup…But then if you’ve done that, it becomes much more complex to remove event listeners since you’ve lost the reference to the component they’re attached to. As a result, you had to store your refs outside of the element, adding a layer of complexity. Now you can just remove event listeners inside your component’s return function because you retain access to the ref inside said return function. This also means we no longer need to worry about the null case in most situations, as React won't call the ref with null anymore when using cleanup functions. The cleanup function itself replaces that functionality, making our code cleaner and more predictable.
useDeferredValue
The purpose of the useDeferredValue hook is to manage computationally expensive operations while maintaining a responsive user interface. It accomplishes this by allowing components to show a previous "stale" value while calculating or fetching new data in the background. A common use case is search functionality that displays results as users type - without deferring, each keystroke could trigger an expensive search operation that makes the input feel sluggish. Using useDeferredValue, the interface can remain responsive by showing previous search results while computing new ones.
This new addition is an important improvement to this hook's initial loading behavior. Previously, the first render would immediately use whatever value was passed to useDeferredValue, potentially triggering an expensive computation right at the start. The new version allows you to provide a lightweight initial value (such as an empty string) that displays immediately, while processing the more expensive value in the background. This means you can show users immediate feedback with a safe default value, then update it with real data once the expensive computation completes. Essentially, this makes useDeferredValue even better for performance improvement.
Document Metadata Additions
These new changes are for adding various metadata tags inside of actual components. There are three options they go over: ,
The main benefit from these changes is that they allow components to truly be self-contained: they can now manage their own metadata along with their styles and scripts. You don’t need to figure out how to lift metadata to the top level or make use of a library to do it anymore, which makes SEO far easier to do. Different pages in an SPA can have different metadata more easily without risking metadata becoming out of sync with the content displayed on the page, which was previously a risk when having different metadata for different pages.
Stylesheet Support
People may have been using tailwind so long that they’ve forgotten, but running non-encapsulated CSS can create issues due to how stylesheets are loaded - namely, load order precedence rules. First, you can end up with flashes of unstyled content - any time the HTML loads and renders before the CSS is finished downloading, it displays as a completely white page, typically in a single column with massive font sizes and native image sizes, which often makes it unreadable.
Additionally, CSS load order rules can create other problems: for example, if you have rules with the same specificity but expect them to load in a certain order. This is relevant, for example, when you have different options for themes (e.g. dark mode). If the dark mode CSS is intended to load last, but your CSS file for dark mode loads first, you can end up with dark mode being light, or some parts of the app where the rules are adhered to and other parts where they aren’t adhered to.
There were a lot of solutions for avoiding this, like CSS in JS, loading all CSS in the
tag, build time bundling CSS together, etc. However, these new CSS changes are intended to help manage these issues by declaratively setting precedence - it also ensures any stylesheets placed in a component are loaded before the component itself renders. It’s also got built in deduping, so you don’t end up loading the same stylesheet multiple times when you reuse components with stylesheet links included.The way you access this new functionality (waiting for the css to load before rendering a component, hoisting CSS automatically to the head, etc.) is pretty simple too - you just need to include a precedence in a particular component. Overall, I’m a typescript enjoyer, so it doesn’t exactly address any particular issues for me, but I’m sure this will be extremely useful large legacy projects.
Async Script Support
This addresses an edge case, as opposed to creating a new core functionality for people to worry about - manually adding/directly managing scripts with a