DEV Community

Safal Bhandari
Safal Bhandari

Posted on

Handling Async State with useRecoilValueLoadable and useRecoilStateLoadable in Recoil

When working with asynchronous selectors or atom families in Recoil, the UI may need to handle multiple states of the async data:

  • Loading (before the data is fetched),
  • HasValue (once data is successfully fetched),
  • HasError (if fetching fails).

Instead of just calling useRecoilValue, which throws a promise internally and lets React Suspense handle it, Recoil provides loadable hooks:

  • useRecoilValueLoadable
  • useRecoilStateLoadable

These hooks give you fine-grained control over async data fetching without fully relying on Suspense.


🔹 useRecoilValueLoadable

useRecoilValueLoadable is like useRecoilValue, but instead of directly returning the resolved state, it returns a loadable object with the following properties:

  • state"loading" | "hasValue" | "hasError"
  • contents → actual value (if hasValue), error (if hasError), or a promise (if loading).

Example:

import { selectorFamily, useRecoilValueLoadable } from "recoil";
import axios from "axios";

// Selector that fetches todos
const todoSelectorFamily = selectorFamily({
  key: "todoSelectorFamily",
  get: (id) => async () => {
    const res = await axios(`https://dummyjson.com/todos/${id}`);
    return res.data;
  },
});

function Todo({ id }) {
  const todoLoadable = useRecoilValueLoadable(todoSelectorFamily(id));

  if (todoLoadable.state === "loading") {
    return <div>Loading todo {id}...</div>;
  }

  if (todoLoadable.state === "hasError") {
    return <div>Error: {todoLoadable.contents.message}</div>;
  }

  return (
    <div>{todoLoadable.contents.todo}{" "}
      {todoLoadable.contents.completed ? "Done" : "Pending"}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

👉 With this, you don’t need to wrap components in React.Suspense.


🔹 useRecoilStateLoadable

useRecoilStateLoadable is similar to useRecoilState, but it works with async selectors or atom families that might be pending. It returns a tuple:

const [loadable, setValue] = useRecoilStateLoadable(myAtomOrSelector);
Enter fullscreen mode Exit fullscreen mode
  • loadable works the same way as in useRecoilValueLoadable.
  • setValue lets you update the atom/selector value.

Example:

import { atomFamily, selectorFamily, useRecoilStateLoadable } from "recoil";
import axios from "axios";

const todosAtomFamily = atomFamily({
  key: "todosAtomFamily",
  default: selectorFamily({
    key: "todosSelectorFamily",
    get: (id) => async () => {
      const res = await axios(`https://dummyjson.com/todos/${id}`);
      return res.data;
    },
  }),
});

function Todo({ id }) {
  const [todoLoadable, setTodo] = useRecoilStateLoadable(todosAtomFamily(id));

  if (todoLoadable.state === "loading") {
    return <div>Loading {id}...</div>;
  }

  if (todoLoadable.state === "hasError") {
    return <div>Error: {todoLoadable.contents.message}</div>;
  }

  return (
    <div>
      <p>{todoLoadable.contents.todo}</p>
      <button
        onClick={() =>
          setTodo({ ...todoLoadable.contents, completed: !todoLoadable.contents.completed })
        }
      >
        Toggle Complete
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

👉 This allows both reading async state and updating it safely.


🔑 Key Takeaways

  • Use useRecoilValueLoadable if you just need to read async state with status control.
  • Use useRecoilStateLoadable if you also want to update that state.
  • Both give access to state (loading, hasValue, hasError) and contents.
  • They are very useful when you don’t want to rely entirely on React Suspense for async rendering.

Top comments (0)