Async Data Loading in React
In JS we can fetch data asyncronously with callbacks, promises, and async/await. For handling errors we could use exceptions or lean on the compiler and use a result type.
We can integrate async data into React components with hooks,
so to fetch a user we might have a hook called useUser
:
function UserPage({ userId }: { userId: string }) {
const user = useUser(userId)
return <div>{user.email}</div>
}
But what about the error and loading states?
Loading and Error States
The data doesn’t appear in component instantly, we need to wait for the server and sometimes the server fails.
So we need to surface some statuses in our API:
initial
– fetch hasn’t started (sometimes combined withloading
)loading
– fetch in progressrefetching
– sucessful fetch before, fetching again (sometimes combined withsuccess
)success
– fetch complete with dataerror
– fetch failed
Tagged Unions aka Sum Types aka Discrimiated Union Types
Aka making the impossible states impossible
type Result<T, E> =
| { type: "initial" }
| { type: "loading" }
| { type: "refetching"; data: T }
| { type: "success"; data: T }
| { type: "error"; err: E }
with some checks against the type
field we can ensure we only render the data
when all the other cases have been handled.
function UserPage({ userId }: { userId: string }) {
const result = useUser(userId)
if (result.type === "initial" || result.type === "loading") {
return <div>Loading...</div>
}
if (result.type === "failure") {
return <div>Failure...</div>
}
return <div>name: {result.data.name}</div>
}
This allows the compiler to check we’ve handled all the variants via narrowing.
Bunch of Bools
Instead of having a singular tag to check against, some libraries opt for using individual boolean fields for each variant.
SWR
function UserPage({ userId }: { userId: string }) {
const { data, error } = useUser(userId)
if (!data) {
return <div>Loading...</div>
}
if (error) {
return <div>Failure: {error}</div>
}
return <div>name: {data.name}</div>
}
One issue is SWR’s typing is isn’t great:
interface SWRResponse<Data, Error> {
data?: Data
error?: Error
mutate: KeyedMutator<Data>
isValidating: boolean
}
Presumably we can’t have both data
and error
, but the types allow it.
React Query
Similar to SWR’s API, except it has proper tagged unions in the return type.
type QueryObserverResult<TData = unknown, TError = unknown> =
| QueryObserverIdleResult<TData, TError>
| QueryObserverLoadingErrorResult<TData, TError>
| QueryObserverLoadingResult<TData, TError>
| QueryObserverRefetchErrorResult<TData, TError>
| QueryObserverSuccessResult<TData, TError>
And as of TypeScript
4.4,
we can even destructure the result and have narrowing work for the
variants (initial
, loading
, success
, refetching
, error
).
function UserPage({ userId }: { userId: string }) {
const { data, status, error } = useQuery(USER_QUERY)
if (status === "loading" || status === "idle") {
return <div>loading...</div>
}
if (status === "error") {
return <div>error: {error}</div>
}
return <div>name: {data.name}</div>
}
While we’re using the status
field for refinement, React Query also returns
boolean flags for each of the variants that we could discriminate off:
function UserPage({ userId }: { userId: string }) {
const { data, isLoading, isIdle, isError, error } = useQuery(USER_QUERY)
if (isLoading || isIdle) {
return <div>loading...</div>
}
if (isError) {
return <div>error: {error}</div>
}
return <div>name: {data.name}</div>
}
Apollo
Apollo has a similar API to SWR and React Query, but the types aren’t great:
interface QueryResult<TData = any, TVariables = OperationVariables>
extends ObservableQueryFields<TData, TVariables> {
client: ApolloClient<any>
data: TData | undefined
previousData?: TData
error?: ApolloError
loading: boolean
networkStatus: NetworkStatus
called: true
}
We have the same issue as SWR where data
and error
are available at the same
time.
function UserPage({ userId }: { userId: string }) {
const { loading, error, data } = useQuery(USER_QUERY)
if (loading || data == null) {
return <div>loading...</div>
}
if (error) {
return <div>error {error}</div>
}
return <div>name: {data.name}</div>
}
React Suspense
While limited in support and experimental, Suspense has a unique API which
surfaces error and loading states by the component suspending (throwing a
promise that bubbles up to React.Suspense
and error boundary components.).
The React docs provide a pretty thorough example and the Relay docs provide more info on error and loading states as well as prefetching data.
Conclusion
I think React Query has the best API and TypeScript definitions of the compared libraries and I hope SWR and Apollo take some inspiration.