We have a render prop based class component that allows us to make a GraphQL request with a given query string and variables and uses a GitHub graphql client that is in React context to make the request. Let's refactor this to a function component that uses the hooks useReducer, useContext, and useEffect.
Class Based Component:
import {Component} from 'react' import PropTypes from 'prop-types' import isEqual from 'lodash/isEqual' import * as GitHub from '../../../github-client' class Query extends Component { static propTypes = { query: PropTypes.string.isRequired, variables: PropTypes.object, children: PropTypes.func.isRequired, normalize: PropTypes.func, } static defaultProps = { normalize: data => data, } static contextType = GitHub.Context state = {loaded: false, fetching: false, data: null, error: null} componentDidMount() { this._isMounted = true this.query() } componentDidUpdate(prevProps) { if ( !isEqual(this.props.query, prevProps.query) || !isEqual(this.props.variables, prevProps.variables) ) { this.query() } } componentWillUnmount() { this._isMounted = false } query() { this.setState({fetching: true}) const client = this.context client .request(this.props.query, this.props.variables) .then(res => this.safeSetState({ data: this.props.normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => this.safeSetState({ error, data: null, loaded: false, fetching: false, }), ) } safeSetState(...args) { this._isMounted && this.setState(...args) } render() { return this.props.children(this.state) } } export default Query
Conver props:
// From static propTypes = { query: PropTypes.string.isRequired, variables: PropTypes.object, children: PropTypes.func.isRequired, normalize: PropTypes.func, } static defaultProps = { normalize: data => data, } // To: function Query ({query, variables, children, normalize = data => data}) { }
Conver Context:
// From static contextType = GitHub.Context ... const client = this.context // To: import {useContext} from 'react' function Query ({query, variables, children, normalize = data => data}) { const clinet = useContext(GitHub.Context) }
Conver State:
I don't like to cover each state prop to 'useState' style, it is lots of DRY, instead, using useReducer is a better & clean apporach.
// From state = {loaded: false, fetching: false, data: null, error: null} //To: import {useContext, useReducer} from 'react' ... const [state, setState] = useReducer( (state, newState) => ({...state, ...newState}), defaultState)
Conver side effect:
// From: componentDidMount() { this._isMounted = true this.query() } componentDidUpdate(prevProps) { if ( !isEqual(this.props.query, prevProps.query) || !isEqual(this.props.variables, prevProps.variables) ) { this.query() } } componentWillUnmount() { this._isMounted = false } query() { this.setState({fetching: true}) const client = this.context client .request(this.props.query, this.props.variables) .then(res => this.safeSetState({ data: this.props.normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => this.safeSetState({ error, data: null, loaded: false, fetching: false, }), ) } // To: useEffect(() => { setState({fetching: true}) client .request(query, variables) .then(res => setState({ data: normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => setState({ error, data: null, loaded: false, fetching: false, }), ) }, [query, variables]) // trigger the effects when 'query' or 'variables' changes
Conver render:
// From: render() { return this.props.children(this.state) } // To: function Query({children ... }) { ... return children(state); }
-----
Full Code:
import {useContext, useReducer, useEffect} from 'react' import PropTypes from 'prop-types' import isEqual from 'lodash/isEqual' import * as GitHub from '../../../github-client' function Query ({query, variables, children, normalize = data => data}) { const client = useContext(GitHub.Context) const defaultState = {loaded: false, fetching: false, data: null, error: null} const [state, setState] = useReducer( (state, newState) => ({...state, ...newState}), defaultState) useEffect(() => { setState({fetching: true}) client .request(query, variables) .then(res => setState({ data: normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => setState({ error, data: null, loaded: false, fetching: false, }), ) }, [query, variables]) // trigger the effects when 'query' or 'variables' changes return children(state) } export default Query