The application I develop at work has been in development for a couple of years, meaning a lot of the code and structure is, sadly, built without hooks.
Although, sometimes we want to include new features to the older parts of the application. Features that are written with hooks.
No problem!
While we cannot use a hook inside a class component, we could use one of two patterns for code reuse that works with them: Higher Order Components and Render Props. And make the hook available through one of them.
In the rest of this post a Higher Order Component will be referred to as a HOC to save me some keystrokes...
We can imagine a useTodos()
hook that loads a list of Todos, and maybe some other stuff as well, that normally would be used like this:
function TodosPage() {
const { data, isLoading, error } = useTodos()
if(isLoading) return <Spinner />
/* etc. */
}
Now let us have a look at how to make this hook available with the two patterns:
HOC
A higher order component is just a function that accepts a component as an argument that will receive some additional props.
function injectTodos(Component) {
const InjectedTodos = function (props) {
const todos = useTodos(props);
return <Component {...props} todos={todos} />;
};
return InjectedTodos;
}
So we just make the function, it accepts the Component to enhance with all the todo information. Inside we make a function component that uses the hook and return that.
We name the function to make InjectedTodos appear in the dev tools instead of just returning that straight away, to make debugging easier.
Now we could do:
class TodosPage extends React.Component {
render() {
const { data, isLoading, error } = this.props.todos;
if(isLoading) return <Spinner />;
/* etc. */
}
}
export default injectTodos(TodosPage);
Great!
Now on to Render props
A render prop component basically hijacks the children properties, replacing that with a function that give you access to additional data or functions:
function TodosData({children}) {
const todos = useTodos()
return children(todos)
}
And now we could put this to use like this:
class TodosPage extends React.Component {
render() {
return (
<TodosData>
{({isLoading, data, error}) => {
if(isLoading) return <Spinner />
/* etc. */
}
</TodosData>
)
}
}
To the ease part
So with not so many lines of code, we could make hooks available in ye old class components. But, imagine that we have several hooks that we would like to make available. We will kind of write the same wrappers again and again, and again to make the hook available through a render prop or a HOC.
To make this transformation easier we could write ourselves two utility functions to convert a hook to either a HOC or render prop.
So for a HOC:
export function makeHOC(useHook, name) {
return function (Component) {
const HOC = function (props) {
const hookData = useHook(props);
const hookProps = { [name]: hookData }
return <Component {...props} {...hookProps} />;
};
HOC.displayName = `${name}HOC`;
return HOC;
};
}
We simply wrap the code to make a HOC with a function that accepts the hook we want to use and what name the props property will be.
I will forward any props to the hook so you could accept arguments to the hook that way.
Also, we do the naming thing, but this time using the displayName
property on our component.
Now to make HOCs of our hooks we simply do this:
const injectTodos = makeHOC(useTodos, "todos")
const injectUsers = makeHOC(useUsers, "users")
And for the render prop:
export function makeRenderProps(useHook, name) {
const RenderProps = function ({ children, ...rest }) {
const hookData = useHook(rest);
return children(hookData);
};
if (name) RenderProps.displayName = `${name}RenderProps`;
return RenderProps;
}
Same here, a function that accepts a hook, and optional name to appear in the dev tools. It will forward every prop, except the children, to the hook.
And the creation of render props components:
const TodosData = makeRenderProps(useTodos, "Todos")
const UsersData = makeRenderProps(useUsers, "Users")
What about hooks that accepts multiple arguments?
Well yes, the above code do have some limitations. If the hook where to need several arguments, not from a single props object, that would not work.
If we were to make the react query library hook useQuery
available through a HOC or Render Props? That hook needs two arguments, an ID and a function returning a promise of data, and a third, optional, options argument.
So we could either make a "wrapper" hook that accepts the props and returns the hook with the properties in the right place:
function useWrappedQuery(props) {
return useQuery(props.queryId, props.queryFn, props.queryOptions)
}
The useWrappedQuery
could then be used by our makeHOC
and makeRenderProps
functions.
Or the makeHOC
/makeRenderProps
functions could accept an additional, optional argument. A function that returns the arguments of the hook. Like this:
export function makeHOC(useHook, name, convertProps = (props) => [props]) {
return function (Component) {
const HOC = function (props) {
const hookData = useHook(...convertProps(props));
const hookProps = { [name]: hookData }
return <Component {...props} {...hookProps} />;
};
HOC.displayName = `${name}HOC`;
return HOC;
};
}
The convertProps
function should return an array that will be spread to arguments in the hook. By the default it will return an array with the props as first, and only, argument. Same as the previous implementation.
Now you could map props from the HOC/RenderProps argument to the hook:
class TodoList extends React.Component { /*...*/ }
const injectQuery = makeHOC(
useQuery,
"query",
props => [
props.queryKey,
props.queryFn,
props.queryOptions
]
)
export default injectQuery(TodoList)
And use this like this
const queryOptions = {retryDelay: 10000}
<TodoList
queryKey="toods"
queryFn={apiClient.todos.get}
queryOptions={queryOptions}
/>
Now the TodoList
component has hook data available in the props query
property.
Or we could also hard code the arguments with this function:
const injectTodosQuery = makeHOC(
useQuery,
"todos",
() => [
"todos",
apiClient.todos.get,
queryOptions
]
}
/* etc. */
What ever solution you like to implement there is a way, and possibilities to "use" hooks inside class components.
Photo by Marius Niveri on Unsplash
Top comments (2)
How do you inject more hooks into the class component?
I assume you are using higher order components, then you can compose multiple higher order components into one component. There are several ready made utility functions for this. For example in lodash (flowRight) and compose if you are using redux.
You can check out this example I made using lodash flowRight and made two hooks available as HOC using my code example.
codesandbox.io/s/compose-higher-or...
Or you can wrap several higher order componens in each other, like this
withTodos(withUsers(App))
But I like the composing approach in the example for readability.
Hope it helps :-)