Building a GraphQL HOC while playing with React Hooks
source link: https://www.tuicool.com/articles/hit/e2Qjmme
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
React hooks are the most exciting and probably the most anticipated feature of React today. The hype around hooks is justified as they help you get rid of the ugly “this” from your React components without sacrificing the statefulness.
Two days before the stable release, I wanted to play with React hooks, just to feel one with the hype. So I built a tiny reusable GraphQL higher-order-component using React hooks.
GraphQL is a good example to try reusable hooks because there is a consistent query and response structure. Also, the data fetching workflow is consistent as you are always querying a single endpoint and it is always a POST request.
We will build a reusable component for <Query>
component but the idea could be used for building HOCs for mutations and subscriptions too. The <Query>
HOC would be used something like this.
const ThoughtComponent = () => ( <Query query={'query { thoughts { id text } }'} variables={{}} > <ThoughtList /> </Query> ) const ThoughtList = ({ data, error, loading}) => { if (loading) { return "Loading" } if (error) { return "Error" } return ( <ListGroup> { data.thoughts.map(({text, id}) => { return ( <ListGroup.Item key={id.toString()}> {text} </ListGroup.Item> ) }) } </ListGroup> ) }
As you see above, you can pass the props query
and variables
to the <Query>
HOC and it’s children would receive the state and response of the GraphQL request.
To achieve the above, I used:
useState useEffect useQuery
Query component using basic hooks
Initializing state
Firstly, we need to initialize our state. Let us use the useState
hook.
const Query = ({query, variables, children}) => { const [graphqlState, setGraphqlState] = useState({ data: null, error: null, loading: true }) }
In the above snippet, we are initializing a new state variable called graphqlState
and its setter method setGraphqlState
that helps you set its value. We have also set its initial value to { data: null, error, null, loading: true}
. We are setting data
, error
and loading
in a single object because we would “always” want to set these values in one go. If they were set differently, we would be causing unnecessary re-renders.
Making a GraphQL Query
Let us now write a function makeRequest
that makes the given GraphQL query and sets the correct state.
const Query = ({ query, variables, children}) => { const [graphqlState, setGraphqlState] = useState({ data: null, error: null, loading: true }) const makeRequest = async () => { try { const response = await fetch( GRAPHQL_ENDPOINT, { method: 'POST', body: JSON.stringify({ query, variables: variables || {} }) } ) const responseObj = await response.json(); setGraphqlState({ data: responseObj.data, error: responseObj.errors || responseObj.error, loading: false }) } catch (e) { setGraphqlState({ data: null, error: e, loading: false }) } } }
We have writen the async function that makes a query to GRAPHQL_ENDPOINT
(initialized somewhere globally) and sets the appropriate state in our state variable graphqlState
. Now, where do we call this function?
Calling an external API is an external-effect in our component, also known as a side-effect
. For such side-effects, we can use the useEffect
hook which is called before and after every render. But there is another problem. We want our makeRequest
function to be called only before the first render, but the useEffect
hook is called before and after every render.
Thankfully, React lets us subscribe our useEffect
to a set of variables, such that, it is called only when those variables undergo a mutation.
useEffect(() => {...}, [variable1, variable2 ...])
We will subscribe our useEffect
only to query
variable so that it is called only before first render.
const Query = ({ query, variables, children}) => { const [graphqlState, setGraphqlState] = useState({ data: null, error: null, loading: true }) const makeRequest = async () => { try { const response = await fetch( GRAPHQL_ENDPOINT, { method: 'POST', body: JSON.stringify({ query, variables: variables || {} }) } ); const responseObj = await response.json(); setGraphqlState({ data: responseObj.data, error: responseObj.errors || responseObj.error, loading: false }) } catch (e) { setGraphqlState({ data: null, error: e, loading: false }) } } useEffect(() => { makeRequest() }, [query]) }
Passing the props to children
Finally, since our <Query>
component is an HOC, we want to pass the graphqlState
to the children components so that they can render based on the values of data
, error
and loading
. So our component will render its children while passing data
, error
and loading
as props. Our final Query
component looks something like:
const Query = ({ query, variables, children}) => { const [graphqlState, setGraphqlState] = useState({ data: null, error: null, loading: true }) const makeRequest = async () => { try { const response = await fetch( GRAPHQL_ENDPOINT, { method: 'POST', body: JSON.stringify({ query, variables: variables || {} }) } ) const responseObj = await response.json(); setGraphqlState({ data: responseObj.data, error: responseObj.errors || responseObj.error, loading: false }) } catch (e) { setGraphqlState({ data: null, error: e, loading: false }) } } useEffect(() => { makeRequest(); }, [query]) return ( <div> { React.Children.map(children, child => React.cloneElement(child, { ...graphqlState }) ) } </div> ) }
Our HOC is ready to be used.
Using the HOC
You can use the HOC with custom components that consume the data provided in props. For example, I have set up a simple GraphQL server at https://bazookaand.herokuapp.com/v1alpha1/graphql
(using Hasura
). Now I can render the results of a query made to my GraphQL server like so:
const ThoughtComponent = () => ( <Query query={'query { thoughts { id text } }'} variables={{}} > <ThoughtList /> </Query> ) const ThoughtList = ({ data, error, loading}) => { if (loading) { return "Loading" } if (error) { return "Error" } return ( <ListGroup> { data.thoughts.map(({text, id}) => <ListGroup.Item key={id.toString()}>{text}</ListGroup.Item>) } </ListGroup> ) }
Extracting reusable logic into custom hooks
As you see, our <Query>
component looks kind of huge. Of course it is smaller than the traditional React component, but still, it looks huge.
Let us extract the querying logic into a custom hook so that:
<Query> <Mutation>
We’ll call our hook useQuery
. It is highly recommended that every hook name starts with use
just for the visual semantics of it. If a hook is named, say, someRandomShit
, it could be a hard time for code readers to realize that it is a stateful function.
Lets extract all our graphqlState
logic into useQuery
and make it return graphqlState
. Our custom hook looks like:
const useQuery = (query, variables) => { const [graphqlState, setGraphqlState] = useState({ data: null, error: null, loading: true }); const makeRequest = async () => { try { const response = await fetch( GRAPHQL_ENDPOINT, { method: 'POST', body: JSON.stringify({ query, variables: variables || {} }) } ); const responseObj = await response.json(); setGraphqlState({ data: responseObj.data, error: responseObj.errors || responseObj.error, loading: false }) } catch (e) { setGraphqlState({ data: null, error: e, loading: false }) } } useEffect(() => { makeRequest(); }, [query]); return graphqlState; };
Now, our <Query>
component can simply use the data from this useQuery
hook and it’ll start looking prettier.
const Query = ({children, query, variables}) => { const graphqlState = useQuery(query, variables); return ( <div> { React.Children.map(children, child => React.cloneElement(child, { ...graphqlState }) ) } </div> ) };
Wrapping up
I wanted to write another section that shows how to use useQuery
in a <Mutation>
component, but the post was getting too long. I personally hate long posts, so its my duty to write short posts on my personal blog :)
Briefly, to use useQuery
for Mutation
component, you would pass another arguement to the useQuery
function called, say, isMutation
. In your useEffect
, you would call makeRequest
only if isMutation
were not true. While returning, your return object would be also have a mutate
key which would correspond to the makeRequest
function. That’s it :)
You can find the source code for this HOC example here .
Hit me up on myemail or Twitter if you have a problem with anything I wrote here or if you wish to discuss more.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK