Introduction
If you have been writing apps with React for a couple of years, you know that React has changed. From React’s first steps using createClass
to the hooks we all know and love, we’ve come a long way, but is it the endgame for React? What are the next steps?
In today’s article, we are going through some new features the React core team has been working on, where we’ll explore new APIs, hooks, and ways to use React and how it will impact our developer experience.
New compiler
When the React team released hooks, some could be used to optimize component re-rendering, like useMemo
and useCallback
. For example:
function Gallery({ title, photos, filter }) {
const filteredPhotos = photos.filter((photo) =>
validateFilter(photo, filter)
);
return (
<>
<h1>{title}</h1>
<PhotoList photos={filteredPhotos} />
</>
);
}
We have a Gallery
component that filters a group of photos based on a filter prop and shows them on the screen. So far, so good. The only issue this piece of code has is that every time this component re-renders, the computation to filter the photos will run again, even when the photos
and the filter
props are the same. In this scenario, filteredPhotos
will also be the same and would not need to be recomputed.
It’s possible to use the useMemo
hook to manually memoize the operation’s result and re-calculate it only if filteredPhotos
dependencies, that is, photos
or filter
props, have changed. So, we can fix the problem like this:
function Gallery({ title, photos, filter }) {
const filteredPhotos = useMemo(
() => photos.filter((photo) => validateFilter(photo, filter)),
[filter, photos]
);
return (
<>
<h1>{title}</h1>
<PhotoList photos={filteredPhotos} />
</>
);
}
Even though the code is more optimized, it brings some issues. First of all, this component is simple, so it is easy to identify where we can enhance the code, but this is not always the case. When you are dealing with more complex code, knowing the best places to make these improvements gets harder and harder. This can make the codebase more challenging to read due to more boilerplate; you don’t know if you’re optimizing too much or too little or if you are doing it in the right place, and depending on the case, this optimization might not even be worth it.
The React core team has been working on a compiler named Forget
to make our lives easier. The main goal is to achieve these performance enhancements automatically so that we no longer need to worry about telling React where to optimize but focus more on what component should be rendered, making our code more concise and clear to read. This gets us closer to React’s original proposal when it first came out to build fast applications and handle the state more easily.
Currently, the compiler has been running in production on the Instagram web application to gather data and validate its progress and design decisions. The good news is that you don’t need to change your code base to run the Forget
compiler. Initially, the compiler was powered by Babel, causing it to also compile parts of the code that are not related to Forget
features, but now it has been changed to accept and keep untouched any JavaScript new syntax that would be compiled by Babel, leaving it to any compilation step after Forget
to modify it if needed.
If you are curious about it check this talk that goes in depth about the Forget
compiler:
Goodbye useEffect
?
Now, what about the most loved feature in React community useEffect
? In previous React versions, developers could execute some code tied to specific component lifecycles with methods like componentDidMount
, componentDidUpdate
, componentWillUnmount
, and so on. Since we no longer have those methods, React introduced the useEffect
hook as a replacement, making it possible to synchronize a component with an external system. But this hook can cause unwanted side effects. Who has never unintentionally created an infinite loop inside a component, right? For this reason, some folks have asked whether the compiler will address these issues associated with useEffect
by eliminating them or at the very least by making it easier to use.
Read it 5 times to make sure my glorious ✨ useEffect ✨ was not on the list
— hahme (@buhama_) February 15, 2024
Saving useEffect for last? Elite boss battle? Hoping for some sick end credits when it happens.
— Ramsay (@Rmmmsy) February 16, 2024
According to the React core team, useEffect
has multiple use cases, so it’s hard to cover all of them with the Forget compiler. So it will have various replacements depending on what is being used for. To illustrate this idea, frameworks like Next and Remix don’t need much useEffect because their routers manage the data fetching. So don’t lose faith, better days will soon come.
Depends on the use case. It won’t just be one replacement. For example, users of frameworks like Next.js and Remix rarely need useEffect because data fetching is managed by the router.
More about this topic here: https://t.co/94lj8uuDmj
— Andrew Clark (@acdlite) February 15, 2024
What is coming next?
Goodbye forwardedRef
If you ever needed to directly interact with the DOM on a React component, you are familiar with ref
and forwardRef
, and every time that we want to send a ref
to a child component, the parent needs to be wrapped in a forwardRef
since React components do not expose by default, which leaves us with this:
const InputText = forwardRef((props, ref) => <input ref={ref} {...props} />);
export default function App() {
const ref = useRef();
return (
<div>
<InputText ref={ref} placeholder="focus on click" />
<button onClick={() => ref.current.focus()}>Focus</button>
</div>
);
}
But soon we won’t need forwardRef
anymore, as ref
will just be a regular prop that no longer needs the bureaucracy of being forwarded; Typescript users rejoice at this news!
const InputText = (props) => (
// ref is being spread here with props
<input {...props} />
);
export default function App() {
const ref = useRef();
return (
<div>
<InputText ref={ref} placeholder="focus on click" />
<button onClick={() => ref.current.focus()}>Focus</button>
</div>
);
}
Welcome React Server Components (RSC)!
Currently, the standard way to write a React component is Client-Side Rendering (CSR)
After all, React is a JavaScript library for the client, right? Well, for many years creating a Single Page Application with React was the default, but then the library and the community matured, frameworks were built around it and even Server-Side Rendering (SSR)
was used by them, with frameworks such as Next and Gatsby taking advantage of it.
In the future, we can expect to write Server Components, which are components that are rendered on the server in a special format called React Server Component Payload (RSC Payload)
and then sent to the client to update the browser’s DOM. The RSC Payload
is binary-encoded data that contains the result of the Server Component Rendering, the placeholders for the Client components, and any props that are passed from Server Components to Client ones.
Here is what it looks like:
import { getNotes } from "./getNotes";
export default async function NoteList({ searchText }) {
// this is our function that fetches notes from the api
const notes = await getNotes({ searchText });
return (
<ul>
{notes.map((note) => (
<li key={note.id}>
<SidebarNote note={note} />
</li>
))}
</ul>
);
}
It almost looks like a regular Client component, but there are two main differences: our component is an async function, so we can also do async operations inside it without having to worry about states or effects, which bring us to the next difference — RSC
cannot use some hooks such as useState
or useEffect
, browserAPIs and event handlers.
There are many advantages of having Server Components: the data fetching is simpler on RSC; our bundle size will be smaller since we will only send the javascript required to run client components; we can expose less sensitive data such as API keys or tokens since they will only be present on the server; we will be able to cache components on our server; and we can have a faster initial page load since we won’t need to wait for all of the javascript bundle to be downloaded to run the page.
While RSCs aren’t officially released, you can try it with NextJs: https://nextjs.org/docs/app/building-your-application/rendering/server-components. Next uses a canary version, therefore it already has access to some of those new features.
React directives
If you are using RSC you will also have to use the new React directives, they are: use client
and use server
.
use client
If you wondered on the previous topic "How does React know whether a component should be rendered on the server or bundled to the client?" It is with the use client
directive! When you add it to the top of a component, like this:
'use client';
import { useState } from 'React';
import { formatDate } from './formatters';
import Button from './Button';
import Wrapper from './Wrapper'
import Editor from './BaseTextEditor'
export default function RichTextEditor({ timestamp, text, saveText }) {
const date = formatDate(timestamp);
return (
<Wrapper>
<Editor date={date} initialValue={text} />
<Button onClick={saveText}>Save Text</Button>
</Wrapper>
}
You are telling React: "Starting from here, every piece of code imported on this file should be included in the client bundle", and that’s it. The Button
component doesn’t even need to have the directive, but by being called by a file that has it, it automatically enters the bundle.
use server
Although the name is similar, use sever
has a different function from use client
. It can be used on async functions to declare that it should be a Server Action
or on the top of a file to declare that every export in that file is a Server Action
, but wait Server Action
? I tricked you, this is not a brief explanation, it’s a whole topic. Let’s check it out!
Server Actions
You might be familiar with submitting data in a form using an onSubmit
handler like this:
function App() {
async function addTodo(e) {
e.preventDefault();
const form = new FormData(e.currentTarget);
const response = await fetch("/api/todo", {
method: "POST",
body: form,
});
// Handle response and other stuff
}
return (
<div>
<form onSubmit={addTodo}>
<input name="todo" />
<button type="submit">Add Todo</button>
</form>
</div>
);
}
With server actions, things are a bit different. We no longer need the submit event handler as we will use the action prop instead, like in traditional applications where the value of this action is a URL that sends the form data. If a URL is passed to the form, it will behave like any regular html form, but, if a function is passed, it will be called with a single argument containing the form data. In this case, we are passing a server action (you can tell that addTodo
is a server action by the directive use server
):
function App() {
async function addTodo(formData) {
"use server";
const response = await fetch("/api/todo", {
method: "POST",
body: formData,
});
// Handle response and other stuff
}
return (
<div>
<form action={addTodo}>
<input name="todo" />
<button type="submit">Add Todo</button>
</form>
</div>
);
}
One of the advantages of doing this is that it allows the form to work before the JavaScript has been loaded or even if it is not enabled on the browser! This is very useful for people with slow devices/connection or that have JS disabled. When this form is submitted, a request will be made to our server, and addTodo
will be executed on the server.
A cool thing about using actions is that React will take care of the life cycle of the data submission and provide a way to access the state and the response of the form action through useFormStatus
and useFormState
hooks.
useFormStatus
// action.js
"use server";
export async function addTodo(formData) {
const response = await fetch("/api/todo", {
method: "POST",
body: formData,
});
// Handle response and other stuff
}
import { useFormStatus } from "react-dom";
import { addTodo } from "./action.js";
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return <button disabled={pending}>Submit</button>;
}
export default function App() {
return (
<form action={action}>
<input type="text" name="todo" />
<SubmitButton />
</form>
);
}
We have a form
rendering a SubmitButton
component containing the hook inside it. The hook must be inside of a child component of the form, if not it won’t work as expected as it looks for a parent form
tag to understand which form it belongs to.
Once you set the hook up correctly, it returns an object with the following properties:
- pending – A boolean value that checks whether the parent “ has a pending submission.
- data – Contains the formData from “.
- method – A string value of either a ‘get’ or ‘post’ representing whether or not the parent “ was submitted with one of those HTTP methods.
- action: A reference to the function passed to parent “ action prop.
In this example, we are taking the pending status to disable the submit button to avoid letting the user submit more data while it is processing. If you are still curious about it, check the docs
useFormState
// action.js
export async function addTodo(prevState, queryData) {
const title = queryData.get("title");
if (title.length < 3) {
return "Todo added";
} else {
return "Couldn't add the todo";
}
}
import { addTodo } from "./action.js";
function App() {
const [message, formAction] = useFormState(addTodo, "");
return (
<form action={formAction}>
<input type="text" name="title" />
<button type="submit">Add</button>
{message}
</form>
);
}
The useFormState
hook helps us get feedback about the result of an action. This hook needs at least two parameters to do the job:
- fn – The function to be called when the form is submitted in the action props.
- initialState – The value we want the state to be initially, like when you are defining a useState hook.
Also, this hook returns an array containing two values:
- The first one is the current state value. Firstly, it holds the value set in the
initialState
parameter and then the action responses afterward. - The action function passed to useFormState as the parameter, with an extra argument of the
initialState
value alongside the usual input values from the “.
useFormState
receives the appTodo
function as the action and an empty string as the initial state. Then, App
renders a form passing the formAction
returned by the hook and a message as well.
When the form is submitted, the action runs and checks if the condition is satisfied. The ‘Todo added’ string renders inside the form if it evaluates it as true. Otherwise, the "Couldn’t add the todo" is the response. You can learn more about it in the official documentation.
use
What if React offered first-class support for promises? That’s one of the use
use cases! use
is a new hook that reads the values from a Promise
or a Context
, and unlike other React hooks it can be used inside of loops and if statements, but still need to be called by a React component or a React hook.
It is a function that receives a promise or context and returns the resolved value of it:
use(Context)
import { createContext, use } from "React";
const ThemeContext = createContext(null);
export default function App() {
return (
<ThemeContext.Provider value={{ color: "dark blue" }}>
<Text hidden={false}>Visible Text</Text>
<Text hidden={true}>Invisible Text</Text>
</ThemeContext.Provider>
);
}
function Text() {
if(hidden) return false;
const theme = use(ThemeContext);
if(theme.hidden)
// changes the text color based on the theme
const className = "text-" + theme.color;
return <p className={className}>{children}</p>;
}
With a regular useContext, the usage of it after if(hidden) return false;
would give an error, but use
makes it more flexible. We can resolve any context with it and we can do it where we want to.
use(Promise)
Imagine that we have a client component that receives a promise from an RSC as prop:
"use client";
import { use } from "React";
export function Message({ messagePromise }) {
const messageContent = use(messagePromise);
return (
<Suspense fallback={<p>waiting for message...</p>}>
<p>Here is the message: {messageContent}</p>
</Suspense>
);
}
In this case, the use
hook will work together with the Suspense
API. When the promise is not resolved it will render the Suspense
fallback, and once it is finished its value will be attributed to messageContent
and that’s it!
Conclusion
We are going through another renassaince of React. New paradigms and features are coming that can cause substantial improvements in our codebases and in the way we write React apps. If you don’t want to fall behind on React, stay tuned for more content, we will be covering all major features from the next release. Thank you very much for your attention and see you later!
We want to work with you. Check out our "What We Do" section!