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:
useRecoilValueLoadableuseRecoilStateLoadable
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 (ifhasValue), error (ifhasError), or a promise (ifloading).
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>
);
}
๐ 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);
-
loadableworks the same way as inuseRecoilValueLoadable. -
setValuelets 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>
);
}
๐ This allows both reading async state and updating it safely.
๐ Key Takeaways
- Use
useRecoilValueLoadableif you just need to read async state with status control. - Use
useRecoilStateLoadableif you also want to update that state. - Both give access to
state(loading,hasValue,hasError) andcontents. - They are very useful when you donโt want to rely entirely on React Suspense for async rendering.
Top comments (0)