loading...
Cover image for Use hooks in class components with ease

Use hooks in class components with ease

stroemdev profile image Simon Ström ・4 min read

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. */ 
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

And use this like this

const queryOptions = {retryDelay: 10000}

<TodoList 
  queryKey="toods"
  queryFn={apiClient.todos.get}
  queryOptions={queryOptions}
/>

Enter fullscreen mode Exit fullscreen mode

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. */
Enter fullscreen mode Exit fullscreen mode

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

Discussion

pic
Editor guide