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 (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);
-
loadable
works the same way as inuseRecoilValueLoadable
. -
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>
);
}
👉 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
) andcontents
. - They are very useful when you don’t want to rely entirely on React Suspense for async rendering.
Top comments (0)