Abhinav Anshul
16 Dec 2022
•
6 min read
React Core team has come up with a new API proposal called the
use()
hook. This new hook will ensure first-class promise support for React components. This proposal has both excited and caused concerns across the developer's spectrum.
In this article, you will learn all there is about this new proposal and why it is so important from an architectural point of view for React applications going forward.
Before diving into the use()
hook, it is important to understand why it was proposed in the first place. In traditional Client-side React components, you usually fetch data as :
import React, { useState, useEffect } from 'react';
const getDataFromAPI = async () => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(data => return data)
}
function App(){
const [todos, setTodos] = useState([]);
useEffect(() => {
getDataFromAPI().then((data) => setTodos(data))
}, [])
return(
<div>
{todos?.map((item) => (
<>
<div>{item?.title}</div>
<div>{item?completed}</div>
</>
) )}
</div>
)
}
Here, the data is being fetched in an asynchronous function outside the React App
component. Later, that function is called in a callback pattern inside the useEffect hook to set the state value of todos
to the fetched data. When you run this app, the component is mounted on the screen(with an empty state value), then the useEffect
hook gets called, updating the state value with the data it has received from the API
. This results in re-rendering the div
elements but with the fetched data this time. All of this happens in split seconds and you might not even be able to visually comprehend all these mounting and re-mounting.
If you are using 3rd party libraries like react-query
, you have a better surface-level API to fetch promise/asynchronous data as,
import React from 'react';
import { useQuery } from "react-query";
const getDataFromAPI = async () => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(data => return data)
}
function App(){
const [todos, setTodos] = useState([]);
const query = useQuery("getTodos", getDataFromAPI)
if (query.isLoading) { return <h1>Loading Todos...</h1>; }
return(
<div>
{query?data?.map((item) => (
<>
<div>{item?.title}</div>
<div>{item?completed}</div>
</>
) )}
</div>
)
}
react-query
simplifies the APIs and prevents you from introducing useEffect
specific bugs.
However, the React core team has an even better solution for this problem. That is, the newly proposed use()
hook.
Using the use()
hook will allow you to have promise support in your components natively without depending on external libraries like react-query
. That is, this hook can read fulfilled promises and load them to your components. use()
hook will allow you to move its loading and error handling to its nearest suspense boundary.
import React, { use } from 'react';
const getDataFromAPI = async () => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(data => return data)
}
function App(){
const todos = use(getDataFromAPI());
return(
<div>
{todos?.map((item) => (
<>
<div>{item?.title}</div>
<div>{item?completed}</div>
</>
) )}
</div>
)
}
Here, the loading
and error
states will be moved to a component that wraps the <App />
component. This way you don't need to explicitly keep a check if various conditions of a promise have been fulfilled or not. React suspense boundary that's available in React 18, takes care of that.
For example, let's say the <Root />
component wraps your
import React from 'react';
function Loader(){
return(
<>
<h2>Loading...</h2>
</>
)
}
function Root(){
return(
<>
<Suspense fallback={<Loader />}>
</Suspense>
<App />
</>
)
}
In the above case, when the use()
hook data is still in the loading state, it would reach out to its suspense boundary and render the fallback UI, until its promise has been resolved.
Please note, the use
hook is not available for production, but if you want to try it before its official release, you can import it as :
import { experimental_use as use } from 'react';
Suppose you have multiple client-side components, and each component is having its own use()
hook, fetching from different APIs, in that case, the loading and error states will only resolve once the promise for all the wrapped components has been fulfilled.
<Suspense fallback={<h1>Loading...</h1>}>
<Component1 />
<Component2 />
<Component3 />
</Suspense>
<Suspense fallback={<h1></h1>}>
<Component4 />
</Suspense/>
In the above example, each component is calling and consuming different APIs using the use()
hook internally, but wrapped under one Suspense boundary. Therefore, none of the components would render until all of them has a fulfilled
promise state.
However, the <Component4 />
will reach out to its own nearest Suspense boundary to handle its promise and it won't affect the first suspense boundary in this case.
This behavior is really useful when using the Suspense
boundary at the same time. It cleans a lot of promise-handling states, that you earlier used to handle with useState
and conditional rendering as,
if(loading){
return <div>Loading...</div>
}
return(
// actual App's UI
)
The use()
hook in theory behaves similarly to await keyword as if you're doing :
const todos = await getDataFromAPI();
Just like an actual async-await
mechanism, the use()
hook wraps the value of the promise returned.
There is however a catch, await
resumes the component execution when the promise wrapped (in our case, getDataFromAPI()
) gets fulfilled. But this is not the case with the use()
hook. It doesn't resume the component execution after the promise has been resolved, instead, it re-renders the same component again.
You can think of it as, during the initial rendering phase, the use()
hook has wrapped the promise that is yet to be fulfilled, but the component execution has already started, therefore it throws an exception at that point in time. As soon as the use()
hook receives the fulfilled promise state, the component rerenders itself, unlike the await syntax that simply resumes the component execution.
Now, ideally, this shouldn't be too big of a problem, as the React component relies on its states and props changes to re-render itself. It won't render the same component twice if its internal state, as well as the props, are the same.
However, in a more practical sense, this is easier to bypass, they are numerous situations where you can build a component with side effects that would make that component re-render. In such a case, you might see a flicker on the screen due to re-rendering. To some extent, this issue would be resolved by having a cache in place by design. React Team in the future will be implementing caching in the use()
API that would prevent re-rendering hence solving the flickering problem.
use()
hook API viable by design?The first and foremost concern that comes to mind is its name itself. use()
hook by definition doesn't tell you what it does, unlike useState(for handling states), useEffect(for handling side-effects), etc.
In the official RFC, developers have raised this concern and proposed different naming conventions such as useWrapped()
, usePromise()
etc.
Arguably, the React team is debating the use()
hook naming convention implies its nature, that it is, "unwrapping" promise events, hence the very generic name. Also, the use()
doesn't break the existing principles of the hooks naming convention, that every hook must start with a use()
keyword. It helps bundlers such as Webpack to let it know that it is a reusable hook.
One important catch is, unlike all the hooks, use()
can be called conditionally, such as under the if-else
, for
loops, etc blocks. This ensures top-level promise chaining, although it breaks the rule of hooks - "hooks should never be called conditiionally"
Although, a little complicated in practice, if this hook comes into effect, you can wrap your App
context as well :
function Home({ isSignedIn }) {
if (isSignedIn) {
const theme = use(ThemeContext);
return (
<>
<button onClick={() => theme}>Change</button>
</>
);
} else {
return <div>Sign In to Continue</div>;
}
}
If you take a step back, Server components were introduced a few months ago, where React components would be rendered on the served side and its Html would simply be sent to the client, just like those old-PHP days.
The good thing about server components is you can use native async await
syntax easily, which is not the case with client-side rendering.
According to the RFC,
"We strongly considered supporting not only async Server Components, but async Client Components, too. It’s technically possible, but there are enough pitfalls and caveats involved that, as of now, we aren’t comfortable with the pattern as a general recommendation."
Hopefully going forward, Client side components would be able to use async-await syntax too, but for now, to handle promises, you have to rely on the use()
hook!
// Server Side components can use native async-await, but cannot use any other hooks on the client side
async function Article({ id }){
const post = await db.articles.get(id);
return(
<div>
<h2>{post.heading}</h2>
<section>{post.content}</section>
</div>
);
}
//To achieve the same result and handle promises on the client side, you have to rely on the new `use()` hook, you can also use any other hooks here, without the async-await
function Article({ id }) {
const post = use(db.articles.get(id));
return(
<div>
<div>{post.heading}</div>
<div>{post.content}</div>
</div>
);
}
That being said, you have reached the end of the article. In this detailed post, you learned all there is to know about the new proposed React hook called use()
, although it is not yet available for production, an RFC coming from React Core Team makes it highly likely that it would be merged with the main branch soon enough and will be available for production.
There have been certain concerns in the React community about its feasibility in the long term and if it would be hard to debug as it is the case with the infamous useEffect()
hook. Nonetheless, having first-class native promise support inside the React component itself is quite exciting and it only improves the Web in general.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!