Due to the nature of my work I end up having to work with a lot of libraries and there's a lot of POC and experimenting that goes on to help junior devs not cry their eyes out when working with code.
This part of setting up good DX for everyone on the team requires failing miserably while doing the same for yourself first.
And the most irritating part about hooks have been the amount of work you have to do to handle async functions which is very common when working with frontend apps and also the reason a lot of companies worked on creating good async hooks, though each of then thought of their usecase of hooks and now that's the standard.
Full disclosure: this post kinda tries to market a tiny npm package I made
Let's take swr
for example, it handles a global state for caching which is
stored by hashing the params given to the useSWR
function and then the same is
implemented in the react-native version
nandorojo/swr-react-native by
Fernando Rojo which makes modifications
for react-navigation. You can use the original
vercel/swr in react-native though the focus
revalidation won't work since the focus logic is different when working with
react-native-navigation.
Back to the point, if I use swr
All amazing features that you'd like to have and has a really simple API, but the only case where the above makes no sense is when you are already working with a library that does most of this.
I work with urql
and
apollo
in almost
every GraphQL backed app that we have and they already provide with hooks and a
connection client that handles caching + network, polling (if needed), doesn't
have revalidation but it's not that hard to set that up.
Now this is where the existence of
react-async comes into picture,
since having 2 network clients maintain cache based on hashing libraries does
hinder performance when working with larger apps and to be fair swr
or
react-query
do not advice you to use them with other clients, they document to
use it with the vanilla graphql client and i'm not really blaming the libraries,
they just don't suffice the usecase.
Now, for the reasons for as to why this wrapper has to exist is, the hooks provided by the libs (url and apollo) are rather messy to work with and get limited to a single query and will need a custom hook implementation when working with multiple data queries or you end up writing a new query that uses fragments from both which ends up with a lot of redundant code
Example:
const FETCH_USER_QUERY = gql`
query fetchUser {
fetchUser {
id
email
name
}
}
`
const FETCH_USER_ADDRESS_QUERY = gql`
query fetchUserAddress {
fetchUserAddress {
street
city
state
country
}
}
`
function ReactExampleComponent() {
// using a hook generated by Apollo for the above query
const {
data: addressData,
error: fetchingAddressError,
loading: isAddressDataLoading,
} = useFetchUserAddress()
// using a hook generated by URQL for the above query
const [addressResponse, refetchAddress] = useFetchUserAddress()
const {
data: addressData,
error: fetchingAddressError,
loading: isAddressDataLoading,
} = addressResponse
// using another hook to fetch user
const {
data: userData,
error: fetchingUserError,
loading: isUserDataLoading,
} = useFetchUser()
return <>{/*...*/}</>
}
This is a very naive example but if you work with something like hasura then
this can get quite common since you end up joining un-related dependent data
quite often for rendering, ending up in writing a lot of useFetch<QueryName>
and handlers for each, or you can write a custom hook to isolate this logic to
be able to reuse it later, something like below
function useUserDataAndAddress() {
// using 2 different hook formats as an example, in reality you'll have just one format based on your lib
const [response: addressResponse, refetch: refetchAddress] =
useFetchUserAddress()
const {
data: addressData,
error: fetchingAddressError,
loading: isAddressDataLoading,
} = addressResponse
const {
data: userData,
error: fetchingUserError,
loading: isUserDataLoading,
refetch: refetchUser,
} = useFetchUser()
return {
data: {
user: userData,
address: addressData,
loading: isAddressDataLoading || isUserDataLoading,
error: fetchingAddressError || fetchingUserError,
refetch() {
refetchAddress()
refetchUser()
},
},
}
}
// and then use it in the following manner
function ReactExampleComponent() {
const { data, error, loading } = useUserDataAndAddress()
if (loading) {
return <Loader />
}
if (error) {
// handle error, show toast, render error page etc
return <></>
}
return <>{/*....*/}</>
}
This is what I normally do and it can get really redundant really quickly since you are basically doing nothing else but organizing the hook data, so I instead decided to go the other route with these libraries (urql and apollo) where they provide a core client that you can use to write fetchers and these are simply functions, so now I can write SDK type functions that I can chain for data so the above code with react-async would look like
async function fetchUserAndAddress() {
const user = await SDK.fetchUser()
const address = await SDK.fetchAddress()
return { user, address }
// will automatically throw an error
}
function ReactExampleComponent() {
const { data, error, loading } = useAsync(fetchUserAndAddress)
if (loading) {
return <Loader />
}
if (error) {
// handle error, show toast, render error page etc
return <></>
}
return <>{/*....*/}</>
}
Thus, reducing my overall redundant code and I write the actuall fetchers just once when I'm writing the gql query, which is also something you can automate if you which to, though I'm fine writing a single line of
// client is the urql client in this case
async function fetchUser(payload) {
return client.query(FETCH_USER_QUERY, { payload }).toPromise()
}
Obviously, the other side is if you work with the traditional graphql server where the queries are limited then yes you can go with the above custom hook approach and you might end up with a few extra custom hooks but that's about it and that's still a clean way to handle async data in your apps.
This just makes it easier since I now have
useAsync(fetcher, options)
The library is still getting used and tested internally at work and so the
options
part isn't available in the released version you can use
@barelyhuman/react-async@beta
to get that and it's typed so you'll get the
options accordingly, but it's still not documented so I'd wait till I release
the next patch version.
To stay updated you can signup on the newsletter or follow on twitter or add this blog's rss , idk your call