So this last weekend I did a bit of exploration around React and Solid’s async primitives. I wanted to see how they compared to each other and how they felt to use.
We are of course talking about React’s use
and Solid’s createAsync
. They seem similar in some regards in that they turn a promise into some kind of value we can then render.
Let’s look at two pretty similar pieces of code, and then I will share a few observations.
// React
export default function Home() {
const book = fetchBook();
const [count, setCount] = useState(0);
return (
<>
<h1>use</h1>
<button onClick={() => setCount(count + 1)}>
Increment ({count})
</button>
<Suspense>
<Book book={book} />
</Suspense>
</>
);
}
function Book(props) {
return <>{use(props.book)}</>;
}
// Solid
export default function Home() {
const book = createAsync(() => fetchBook());
const [count, setCount] = createSignal(0);
return (
<>
<h1>createAsync</h1>
<button onClick={() => setCount(count() + 1)}>
Increment ({count()})
</button>
<Suspense>
<Book book={book()} />
</Suspense>
</>
);
}
function Book(props) {
return <>{props.book}</>;
}
Observations
- Let’s start with the
Book
component. They are both pretty similar but the thing I found interesting is that in Solid’s version it doesn’t actually care whether the value is async. It’s treated simply like a signal and doesn’t need theuse
to unwrap it. - Moving to the
Home
component, the most obvious difference I found is that the React version calls thefetchBook
function every time the state is updated whereas the Solid version doesn’t. - Another thing to note is that the Solid version wouldn’t require the extra
Book
component. The React one does for theuse
to work properly. You also need to call the promise outside the suspense boundary and pass it in. - Lastly, moving the
createAsync
into theBook
component would be trivial in Solid where React would require creating a new component.
Note
After writing this I realized you don’t actually need the use
when rendering out a promise in the JSX. I think maybe React does this automatically when you render a promise. This makes some of my above observations not apply.
What about Server Components?
So you might be wondering how does server components fit with use
. Below is an example of how RSC would work:
// page.tsx
export default function Home() {
const book = fetchBook();
return (
<>
<h1>rsc</h1>
<CounterButton />
<Suspense>
<Book book={book} />
</Suspense>
</>
);
}
// Book.tsx
"use client";
export default function Book(props) {
return <>{use(props.book)}</>;
}
// CounterButton.tsx
"use client";
export default function CounterButton(props) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Increment ({count})</button>
</>
);
}
Observations
- Book looks exactly the same and doesn’t really change
- The above setup fixes the issue where the
fetchBook
call was being done over and over again on state change. This is becauseHome
is now a server component that doesn’t rerender. It just passes the promise from the server to the client for theuse
to pick up. Normally you wouldn’t do this and you would justawait fetchBook();
in the server component and pass the data to the child. - Notice we again need another component with
use client
to hold our counter state. This is because once againHome
is a server component that doesn’t have any notion of client state.