Introducing the container pattern – a better alternative to Redux

Using a container metaphor to provide a minimal and modular microservice-esque solution to managing state.

Introducing the container pattern – a better alternative to Redux

Redux would've been a fine solution for state management two years ago. But, these days, with how mature the React Context API has become, is it really a good choice anymore?

I'd argue it's not, simply because the alternatives have gotten so good.

Today, I'm going to share one such alternative. This is a design pattern I nickname the "Container" pattern that uses a library called unstated-next. Although, to be fair, the unstated-next library is really just an abstraction on React Context.

But, the real benefits come from the code organization that can come about as a result of this. Today, I'm going to share how to implement this.


What’s the Container Pattern?

The container pattern is a methodology in which instead of having all your global state in one global store, such as Redux, you divide that state into multiple chunks called containers. These chunks are responsible for managing their own state and can be pulled into any functional component in the app, using something similar to the following syntax:

const {user} = Auth.useContainer();

This pattern works really well. It divides state into self-managing chunks, rather than having everything intertwined. Each component can simply pull in the chunk of state that it wants to use and is only dependent on a part of your applications state.

Each chunk of state is easy to reason about. They’re simply a custom hooks wired up to context providers — that’s it. The term “Container” really just means “a React Custom Hook and a Context Provider.” So when someone is recommending state management with Hooks and useContext, they’re technically recommending this container pattern.

To use containers you just have to import the Context and use the hook. You don’t technically need any external libraries, however I use a library called Unstated-Next because it gives me some benefits that make this pattern even easier.


What is Unstated-Next?

Unstated-Next is a tiny library that helps us reason about these global containers a little bit easier. This library is tiny (like 200 bytes tiny) — for good reason, it basically doesn’t do anything that React’s Context API doesn’t already do.

This library is 100% optional for this design pattern. It just provides some small API improvements that make Context easier to work with. Some of the main benefits include:

  • Type-Checking: This gives you typescript support out of the box. This was one of my gripes with using the React Context API, so it’s nice to see that unstated-next solves the issue.
  • Error Handling: If you try to access a container that doesn’t have a Context provider above it in the React DOM tree, it will throw an error. This is a life-saver for debugging.
  • Easier to Think About: Thinking about contexts can seem abstract at times, but using this library with the mental concept of “containers” is a lot easier.

What Does This Pattern Look Like?

File structure

When I use this pattern, I put all my containers in a “container” folder at the root of the src directory. I suffix each container with the word “Container” and have all the relevant code in one file.

This already has benefits over something like Redux, where a single responsibility might be divided over three or four files for the actions, reducer, store, selectors etc.

The container file

The container is where your slice of state will live. This file contains everything necessary for reading and writing to this part of state. Here’s what a container file may look like for an AuthContainer:// The reducer. This would be very similar to your reducer in Redux.

// The reducer. This would be very similar to your reducer in Redux.
// This is optional, you can just use useState instead, but this is
// here to show that if you want to use a reducer and do more
// complicated state transitions you can.
function authReducer(state: AuthState, action: Action) {
   {/* ... */}
}
// Custom Hook
function useAuth(initialState: AuthState) {
   const [state, dispatch] = useReducer(authReducer, initialState);
const loginWithGoogle = () => {
      dispatch(loggingIn());
      doGoogleLogin()
         .then(user => dispatch(success(user)))
         .catch(err => dispatch(error(err.message)));
   }
const loginWithEmailPassword = (email, password) => {
      dispatch(loggingIn());
      doEmailPasswordLogin(email, password)
         .then(user => dispatch(success(user)))
         .catch(err => dispatch(error(err.message)));
   }
const logout = () => dispatch(logout());
return { 
      user: state.data,
      isAuthenticating: state.loading,
      error: state.error,
      loginWithGoogle,
      loginWithEmailPassword,
      logout
   };
}
// Create the Container (this can be a Context too)
// You just pass in the custom hook that you want to build the
// container for.
export const Auth = createContainer(useAuth);

This is really clean — it’s basically just a custom hook and then that one little line at the bottom to make it a container. When you add that container code at the bottom, it makes this custom hook have the same state, even if used in multiple different components. This is because the Unstated-Next containers just use the Context API under the hood.

To make that work you first need to add a Store to your application which will store all the containers. This might look something like this:

Note: I think there could be a better way to manage a Store like this. If we could dynamically create this structure based on an array of containers, or something like that, I think that would be a lot cleaner.
Also, if there was a way to make all these load at the same level of the DOM so any container could access any other container, that would be amazing too. Sadly I think that’s a limitation of React.

Put this in the root component, so your root component looks something like this:

const App: React.FC = () => {
   return (
      <Store>
         <ReactRouter>
            <AppRoutes>
         </ReactRouter>
      </Store>
   );
}

And voila! If you did this correctly, you should now be able to go into any of your React components and use this hook like this:const LoginPage:

const LoginPage: React.FC = () => {
   const {
      formLogin, 
      googleLogin, 
      isAuthenticating,
      user
   } = Auth.useContainer();
   useEffect(() => {
      if (user) {
         history.push('/home');
      }
   }, [user]);
   
   return (
      <div>
         <button onClick={() => googleLogin()}>
            Login with Google
         </button>
         ...
      </div>
   );
}

If you did everything right, following this pattern should work! If you did something wrong Unstated-Next might throw an error that says that the container’s provider hasn’t been created. But that’s good because it’s an explicit error message for a bug that would be really difficult to track down if you were using the basic React Context!


Why Not Use Redux?

Redux is great for state management at a large scale. It’s the tried-and-tested way to manage state for large applications. However, for the vast majority of applications out there, Redux is the wrong place to start. It’s very boilerplate heavy and probabbly won’t give you many benefits unless you already know you need it.

So I’m offering this pattern as an alternative.

The main benefit you get from this pattern is that it makes more sense from a developer’s perspective. Redux takes your state and pulls it away from the view layer. I’d argue that a better way to manage state would be to colocate it with the view layer that uses it.

This is why React Hooks exist.

You can already see things moving towards this methodology with the movement of other pieces of state out of things like Redux and into hooks:

So, it makes sense that the global state also be built to fit well into a hook ecosystem.

The majority of my state management is done by various hook libraries, so it makes sense that my global state management also be hook-centric.

The container pattern implements this idea. It offers the majority of the functionality as Redux at a fraction of the time-cost and is designed with hook-centric development in mind.

For any small-medium sized-project, this pattern is a no-brainer for me. For a larger project, it depends on the use-case.

Here are some comparisons between the container pattern and Redux:

The Container pattern has the following benefits:

  • Less boilerplate than something like Redux.
  • Uses the native Context API under the hood.
  • You can learn the API in 10 minutes if you know useState, useContext and Custom Hooks.
  • Only uses one tiny library, and even that dependency is optional.

It also has the following cons:

  • No support for middlewares.
  • No tool akin to the Redux chrome debugger.
  • Containers must be provided in a certain order if they have dependencies on each other.

With this in mind, hopefully, you now have a better idea of what alternatives there are if your use-case doesn’t need something as bulky as Redux.

If you want to employ this pattern but can’t quite leave Redux, another alternative would be a using Redux Toolkit and a Redux Ducks Pattern.

Redux Ducks

This Redux Ducks approach works well if you’re building a large application because it uses a container-focused methodology, but still keeps you in the ecosystem of Redux.


Conclusion

That’s the Container pattern. If you’re looking at using Redux in an app, I would take a serious look at the cost of doing so to determine if your application actually requires it. I think this pattern is a good place to start regardless, and because it’s so small and modular you can easily migrate it into Redux in the future.

Overall, this pattern has helped me clean up my codebase a lot. It’s also helped me remove state management from my list of pain-points when developing applications.

Let me know what you think — hopefully it will work well in your projects. Enjoy!