Refactor a render Prop Component to a Custom React Hook

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 2 years ago

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.

Instructor: [00:01] Now, this query component is a render prop API. That makes it really nice, because we can declaratively use this query component, passing our query variables and our normalize function, and then get the state and render based off of that state.

[00:15] It's a really nice API, but it is a little bit nested. What if we could actually turn this query into a custom hook? This query isn't actually rendering anything. What if, instead of calling the children prop that we're passing it, it actually just returned these items of state?

[00:32] Let's take a look at what that might look like. We're going to need these items of state. We'll just pull these out right here. I'll say const those. We'll destructure those off of useQuery, and then we'll want to pass all of these values to useQuery.

[00:45] We'll just JavaScriptize those really quick. Variables will be an object, and then our normalize will be this normalize user data. Cool, and then with that, let's take this error all the way down to this closing parentheses.

[01:02] We'll go from here to here and paste that in. Wow, that's a lot less nested. It's actually quite a bit easier to read. If we multiple of these render prop-based APIs, we could maybe change them to be hooks as well, and have even less nesting.

[01:18] Our tree would be a lot smaller, which would be good, too. Sweet. Let's go ahead and make this happen. I'm going to take this useQuery, I'm going to scroll up here to the top, where I'm importing query, and I'm going to do a named import for useQuery.

[01:31] Now, let's go back to our query component here, and let's make this a custom hook. Instead of query, we're going to do useQuery. Instead of children, we're going to just simply return the state. Then we can have export useQuery.

[01:50] Now, we probably aren't going to be able to refactor our entire application to be using this new custom useQuery. We probably will still have some areas of the application that need to have a render prop-based API of this query component.

[02:04] Let's go ahead and make that. We'll just say const query equals a function component that has children. Then it takes a bunch of other props, as it was before. Here, we'll just call children with useQuery props.

[02:17] That's identical to what we had before. We've done a simple refactor. Nothing has changed for the users of this query component. If we save that, we can even take a look at our tests, and our tests are still passing as well.

[02:31] Our tests are actually using our query component. Actually, this is the way that I prefer to test custom hooks, is by creating a render prop-based API out of them, and then rendering that render prop-based API, and testing it like a regular React component.

[02:49] In review, to make this work, we went to our user situation. We took all the contents of the children prop, the things that we were returning from our children function, and we simply returned that. Then we got the data we needed from the useQuery, from the custom hook version of our render prop-based component.

[03:08] We plucked off the data that needed in our render, and we provided as an object, the options that this custom hook needs. Then to make that work in our custom hook, we simply rename this useQuery. We removed the children prop, and we treated this as a regular options object.

[03:26] Then instead of returning a call to children and passing the state, we simply return the state. Then to make this easier to test, and to ensure other places in the application can continue to use the render prop-based query component, we created a very simple render prop-based query component that simply uses the useQuery hook.

~ 4 years ago

Great course as always, two points I would like to ask you expand on if you ever had time:

  1. getFooProps pattern (from your previous course) with hooks
  2. examples of useMemo to optimize components

Again, thank you and looking forward to your next videos!

~ 4 years ago

Another thing - how would you handle defaultProps, propTypes etc for React functions that return state (these don't seem to work)?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Hi Platon! You mean how do you use propTypes and defaultProps for custom hooks?

For propTypes I recommend using TypeScript or Flow instead.

For defaultProps, use default values, just like we do with the normalize function here:

function useQuery({query, variables, normalize = data => data}) {
  // ...

I hope that helps.

~ 4 years ago

Thanks, that helped.

Have you thought of making some educational material on implementing the getProps pattern from your previous course with hooks, or the useMemo for optimizing components with the return state (along with component as default export) pattern from this course?