31

Building a GraphQL HOC while playing with React Hooks

 5 years ago
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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK