Decouple from Redux using Hooks

Decouple from Redux using Hooks

Received wisdom in the react community holds that you should subdivide your components into 'smart' containers and 'dumb', presentational components.

The rationale is to separate concerns. Logic and behavior such as data fetching, any interaction with the outside world, dispatching actions and other side effects go into our smart container and what our UI should look like as a function of the resulting data into our dumb component.

This idea leads to a pervasive pattern of creating container components solely for the purpose of connecting a part of the component tree to the redux store. So we end up with two components; one in a containers folder fetching data from the store and passing down actions and the actual component in the components folder.

To me, this quickly felt cumbersome and rigid, if I simply wanted a component to have access to a slice of the store, I found myself having to create an intermediary container and changing a number of imports in other files that use the component.

I also stopped putting every bit of state into the redux store and instead took advantage of react's new and improved context api to co-locate state that is confined to a specific, well-delineated part of the component tree. This raised questions such as whether consuming context should also only happen inside containers.

Besides, what are we really achieving by this kind of separation? Concerns about data access still has us change a number of files in the component tree and the hierarchy of our UI seems to dictate which components should be containers (by default the top level one).

While well-intentioned, the benefit of decoupling UI from state and behavior does not seem to warrant the overhead and complexity introduced by organizing files this way.

Luckily, we have a perfect tool to decouple data and behavior from our presentational components...

Hooks!

And wouldn't you know react-redux lets us consume its API only using hooks.

Let's look at a small (and admittedly contrived) example. Say, we want to implement a toggle button and keep the toggle state in the redux store, maybe it needs to be available globally, toggling an app wide setting.

This is what such a component could look like using redux classico:

import React from "react";
import {connect} from "react-redux";
import {toggleAction} from "./store/toggleActions";

const Toggle = ({on, toggle}) => {
  return (
    <button onClick={toggle}>{on ? 'on' : 'off'}</button>
  );
};

const mapStateToProps = state => ({
  on: state.toggle.on
});

const mapDispatchToProps = {toggle: toggleAction};

export default connect(mapStateToProps, mapDispatchToProps)(Toggle);

Yes, we probably want this to be a container components wrapping a presentational component (e.g. a button) simply passing on and toggle down via props, but for the sake of simplicity we're keeping everything in one component.

Now let's refactor this to use the new redux hooks api:

import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {toggleAction} from "./store/toggleActions";

const Toggle = () => {
  const on = useSelector(state => state.toggle.on);
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch(toggleAction())}>{on ? 'on' : 'off'}</button>
  );
}

Not much of an improvement, we reduced some boilerplate, but there is still a lot of redux code sitting in our component.

The beauty of hooks is how composable they are, we can just create a custom useToggle hook:

import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {toggleAction} from "./store/toggleActions";

const useToggle = () => {
  const on = useSelector(state => state.toggle.on);
  const dispatch = useDispatch();
  const toggle = () =>  dispatch(toggleAction());
  return [on, toggle];
};

const Toggle = () => {
  const [on, toggle] = useToggle();
  return (
    <button onClick={toggle()}>{on ? 'on' : 'off'}</button>
  );
};

Now our component knows nothing about redux, we did not need to create a Toggle container or some abstract HOC wrapping our button, we simply use a hook to encapsulates the data layer.

This way our components are also closed to modification, should we decide to employ a different state management solution. Moving redux state into react context simply involves rewriting the hook (at least for consumers of the context):

import ToggleContext from './ToggleContext';
const useToggle = () => {
  const {on, toggle} = useContext(ToggleContext);
  return [on, toggle];
};

As I already alluded to, another disadvantage of the container pattern is that often the top-level component ends up being the container that fetches a slice of state from the store and passes it down to its children as props.

Take as an example a BookList container component that simply iterate over an array of books from the store and renders a BookItem in a list:

import React from "react";
import {connect} from "react-redux";

const BookItem = ({title, author}) => {
  return (
    <div>
      <h1>{title}</h1>
      <h2>{`by ${author}`}</h2>
    </div>
  );
};

const BookList = ({books}) => {
  return (
    <ul>
      {books.map(({book}) => {
        return (
          <li key={book.id}>
            <BookItem {...book} />
          <li>
        );
      })}
    </ul>
  )
};

const mapStateToProps = state => ({
  books: state.books.index
});

export default connect(mapStateToProps)(BookList);

A problem we might run into is that, if one book in the list is updated the entire list re-render which can quickly turn into an annoying performance issue. That is why it's a good practice to provide data as close to where it is needed as possible.

Instead of having to go in and add a BookItem container, we can just create a custom hook.

First BookList only receives an array of book ids, which presumably change less frequently than an any particular book:

import React from "react";
import {connect} from "react-redux";

const BookList = ({bookIds}) => {
  return (
    <ul>
      {bookIds.map(({bookId}) => {
        return (
          <li key={bookId}>
            <BookItem id={bookId} />
          <li>
        )
      })}
    </ul>
  );
};

const mapStateToProps = state => ({
  bookIds: state.books.ids
});

export default connect(mapStateToProps)(BookList);

The BookItem then uses the book id to fetch its data from the store:

import React from "react";
import {useSelector} from "react-redux";

const BookItem = ({id}) => {
  // we would normally pass a selector function here
  const book = useSelector(state = state.booksById[id]);
  return (
    <div>
      <h1>{book.title}</h1>
      <h2>{`by ${book.author}`}</h2>
    </div>
  );
};

We can neatly bundle that and even add the action creator for updating a book in a custom useBook hook:

// src/store/hooks.js

const useBook = (id) => {
  const book = useSelector(getBook(id));
  const dispatch = useDispatch();
  const update = (...args) => dispatch(updateAction(id, ...args));
  return [book, update];
}

Depending on how you structure your react redux projects you can include this hook as part of your redux-duck or export it alongside actions and selectors inside your redux or store folder.

It is now easy to import a hook to consume data from our redux right where it is needed profiting from the above mentioned performance gains.

What is more, we effectively removed any trace of redux from our components, granted we still need to wrap everything in a Provider, but the overall footprint is vastly reduced. Now, wherever the tempestuous winds of the javascript ecosystem may carry you, you have a clean way of interacting with any state management solution you choose in the future given it exposes hooks that you can compose.

Ideally hooks allow all our components to be dumb.