Skip to content

Battle of the Asyncs

Posted on:July 27, 2024

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

  1. 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 the use to unwrap it.
  2. Moving to the Home component, the most obvious difference I found is that the React version calls the fetchBook function every time the state is updated whereas the Solid version doesn’t.
  3. Another thing to note is that the Solid version wouldn’t require the extra Book component. The React one does for the use to work properly. You also need to call the promise outside the suspense boundary and pass it in.
  4. Lastly, moving the createAsync into the Book 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

  1. Book looks exactly the same and doesn’t really change
  2. The above setup fixes the issue where the fetchBook call was being done over and over again on state change. This is because Home is now a server component that doesn’t rerender. It just passes the promise from the server to the client for the use to pick up. Normally you wouldn’t do this and you would just await fetchBook(); in the server component and pass the data to the child.
  3. Notice we again need another component with use client to hold our counter state. This is because once again Home is a server component that doesn’t have any notion of client state.