Our <Query />
component is a render prop based component that the <User />
component uses. But because it doesn't render anything, we can actually just change it to a custom hook. Let's create a useQuery
hook that returns the state from the hooks the Query component uses and use that instead. But we'll preserve the component so we don't have to refactor everywhere that uses the Query render prop based component as well and we can keep our tests passing as they are.
function Query({query, variables, normalize = data => data, children}) { const client = useContext(GitHub.Context) const [state, setState] = useSafeSetState({ loaded: false, fetching: false, data: null, error: null, }) useDeepCompareEffect( () => { 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], ) return children(state) } function User({username}) { const {logout} = useContext(GitHubContext) const [filter, setFilter] = useState('') return ( <Query query={userQuery} variables={{username}} normalize={normalizeUserData} > {({fetching, data, error}) => error ? ( <IsolatedContainer> <p>There was an error loading the data</p> <pre>{JSON.stringify(error, null, 2)}</pre> </IsolatedContainer> ) : fetching ? ( <LoadingMessagePage>Loading data for {username}</LoadingMessagePage> ) : data ? ( <UserContext.Provider value={data}> <Container> <Row> <Column width="3"> <Profile /> <PrimaryButton css={{marginTop: 20, '100%'}} onClick={logout} > Logout </PrimaryButton> <ButtonLink css={{marginTop: 20, '100%'}} to="/"> Try another </ButtonLink> </Column> <Column width="9"> <Text size="subheading">Repositories</Text> <RepoFilter filter={filter} onUpdate={setFilter} /> <RepoList filter={filter} /> </Column> </Row> </Container> </UserContext.Provider> ) : ( <IsolatedContainer>I have no idea what's up...</IsolatedContainer> ) } </Query> ) }
Refactor code into hooks:
function useQuery({query, variables, normalize = data => data}) { const client = useContext(GitHub.Context) const [state, setState] = useSafeSetState({ loaded: false, fetching: false, data: null, error: null, }) useDeepCompareEffect( () => { 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], ) return state } const Query = ({children, ...props}) => children(useQuery(props))
We add API 'useQuery' it just return state, instead of rendering the children. And we still keep 'Query' component in case somewhere in the code, we still need to use render prop partten.
function User({username}) { const {logout} = useContext(GitHubContext) const [filter, setFilter] = useState('') const {fetching, data, error} = useQuery({ query: userQuery, variables: {username}, normalize: normalizeUserData, }) return error ? ( <IsolatedContainer> <p>There was an error loading the data</p> <pre>{JSON.stringify(error, null, 2)}</pre> </IsolatedContainer> ) : fetching ? ( <LoadingMessagePage>Loading data for {username}</LoadingMessagePage> ) : data ? ( <UserContext.Provider value={data}> <Container> <Row> <Column width="3"> <Profile /> <PrimaryButton css={{marginTop: 20, '100%'}} onClick={logout} > Logout </PrimaryButton> <ButtonLink css={{marginTop: 20, '100%'}} to="/"> Try another </ButtonLink> </Column> <Column width="9"> <Text size="subheading">Repositories</Text> <RepoFilter filter={filter} onUpdate={setFilter} /> <RepoList filter={filter} /> </Column> </Row> </Container> </UserContext.Provider> ) : ( <IsolatedContainer>I have no idea what's up...</IsolatedContainer> ) }