Pattern for Encapsulating Stateful Front-end Logic with React Hooks with Cross Repo Support
Hooks are a powerful tool that can be used to solve the difficulty of encapsulating and sharing stateful logic across apps. It can be used as an alternative to Redux. This article explains how hooks can be used to achieve clean encapsulation and it comes with patterns and examples to aid adoption.
The last part of the article will explain a brand new approach to build front end application that is centered around context instead of global Redux store.
Encapsulation Problem with Redux
Redux is extremely successful as a predictable state container. However it is very verbose as soon as the global state shape needs to change. Once I needed to move a single isOpen boolean from Component state to Redux store. You would think it is a matter of moving a few lines from one place to another, but I ended up a diff across 14 files totaling +214,-61 lines because I needed to write a new action, a new reducer, a new selector, augment mapStateToProps, augment mapDispatchToProps, remove component state, and add test for each of them all because of poor encapsulation of Redux Stateful Logic. At the end of it, I realized that this isn’t far from a nightmare.
I made Figure 1 to demonstrate the daunting encapsulation scheme for Redux. There are simply too many things that is required to encapsulate and glue the stateful logic.
In the case above, what we need ideally is encapsulation of the isOpen stateful logic. The stateful logic should have a single API that contains getIsOpen and toggleIsOpen. The API can have multiple implementation that differs in where isOpen is stored or what side effect is called but not the API, thus decoupling how we use the API from the implementation details of the API. See Figure 2.
This article explains how to use Hook to deliver the idealistic encapsulation of stateful logic. The remainder of the article is going to explain the approach step by step.
Introduction to React Hooks
We are going to dive a bit into basic Concepts: Hook, Function and Function Component. Feel free to skip this section if you are already familiar with it. More complete React Hooks tutorial is [Linked].
A Hook is a function that calls the React hook API directly/indirectly. Hooks are customary prefixed with “use”. Hooks can only be executed in a function component at the top level, though you can pass hook as parameter anywhere. Below is a basic hook example:
Any function that returns React.Node can be used as a Function Component. Hooks that return React.Node are also a Function Component.
Now it is time to do some exercise:
Exercise 1: Draw the Venn diagram for hook and function. Draw the Venn diagram for hook and Function Component.
Exercise 2: Should you write Class Component if you can solve with Function Component? Should you write a hook if you can solve it with a function? Should you write a component if you can solve it with a hook?
Answer for both are at the end of article.
Use React Hooks to Encapsulate Stateful Logic
Figure 3 illustrates that using React Hooks, we can encapsulate the stateful logic into a context provider, and hooks. There is no glue logic to write, and the component will find the context provider by itself and exhibit the look, feel and behavior configured by the context provider. The glue logic noted in grey here is merely reference of API exported from a stateful logic.
If we move a single boolean from state to context, it will merely be replacing one hook that call useState() inside to another hook that call useContext to use the state in the context. Both hook will return the same API that is part of the encapsulated interface.
Even better, when using React Hooks based stateful logic, if the related context provider is absent, the logic can be designed to implement some default behavior, thus allowing you to build stateful logic into UI components that is backwards compatible with current generation of UI components that is not context aware.
For example, Material UI can be designed to automatically use Google user account configuration to change its look, feel and behavior if specific context provider existed, but will still render the default Material UI if Google user account configuration is not available in the required Context. Without this capability, Material UI will need to have various props that allow customization and each app needs to have its own logic that reads Google user account configuration and use that to map to a Material UI pro that customize look, feel and behavior. Having the option to publish customization logic in open source library easily due to superb encapsulation is not available in Redux.
The new capability is not limited to customization. Any stateful logic can be released via open source library with a default behavior in the case the required context provider doesn’t exist.
Build Apps around API
When we have a tool like hooks, we are likely to re-architect how we write software completely. I’d advocate a way to build dependency injection with API instead of via mapStateToProps and mapDispatchToProps (using Redux).
The API is a replacement for selector and action creator in Redux. The API bundles related selectors and action creators together and passes them between components. Compared with Redux’s method of using mapStateToProps and mapDispatchToProps, API-based dependency injection provides a proper reusable abstraction instead of typical Redux practice of bundling unrelated selectors together, and bundling unrelated action creators together.
The example below is a block diagram that describes two stateful logic blocks. The MessageProvider provide multiple TargetingApi that will trigger Modal, Popout or Banner on screen. The TargetingApi is consumed and again provided by StateMachineProvider to sequence through a series of Tooltips if user is targeted for a specific message. Now we have a single Targeting API defined once and used in many places.
Show me the Code
With all the talks, I still need to show code. The working code is currently open source at box-ui-elements. In the example. MessageApi provides messages from backend to front end. TargetingApi provides a universal way to alter behavior of component. The MessageProvider stateful logic translate MessageApi to TagetingApi, thus allowing in app messages to target any component.
Patterns to add Hook to Existing Redux App
Hook can only be called at top level of a Function Component. Because of restrictions of hook, it is often confusing of how to use it or it is even powerful enough. I provide three ways to use Hook in a Redux App that should fit most of the use cases. Adopting these patterns help you to focus on solution quickly. The first two methods are recommended and you should stick with them. The third HOC method should only be used for legacy class component.
Method 1: Hook imported 👍
Create a function component that directly import hook and use it. For this method, the component that call the hook has to be functional component. The hook is bound to the component statically.
Method 2: Passed in Hook as Prop 👍
Hook can only be called from top level of a Function Component, but you can reference it anywhere and pass it as a hook prop to a component that accepts hook prop. With this approach, hook can be dynamically bound to a component based on where it is used. Like the example below.
It will be Tooltip’s responsibility to call useTargetingApi at top level of a function component. It is possible Tooltip itself is still a class component and Tooltip pass the hook future down but the component that execute the hook must be functional component.
For testing, we can assign useTargetingAPI to be a function.
Method 3: Hook injected from HOC
This method is very much like Redux Container. A HOC is used to inject the dependencies, but one of the issue is complexity of the code, and it is hard to typing the HOC (impossible for current version of flow.js if the WrappedComponent has optional props).
One of the main learning is with hook, it is very easy to inject dependencies but it is very hard with Redux that requiring HOC to perform this.
Method 4: Ad Hoc Hook
This approach is attached here to show how flexible hook dependency injection can be. It might not be the pattern you use in production. Figure 5: 1/2 is what it looks like after we introduce hooks, and Figure 5: 2/2 is what it looks like before we introduce hooks. With this approach, hook can still get injected prop from Redux as it is inside the container.
The sample code for before adding hooks is in Example 4 below:
The sample code for after adding hooks is in Example 5 below:
Pay attention to the difference of what is passed to shallow in two tests. The first test shallow renders both function and React Component. The second test only shallow renders the Function Component.
How Hook Impact Re-render
There is always some concern of how state passed down through hook will properly trigger re-render, so I felt it is worth explaining. Let’s first talk about what causes a component to re-render. A component is re-rendered if one of the three happened:
- Prop changed
- State changed
- Context value changed
In a purely Redux app, the only context is Redux store which is a context. Only a Container has access to the Context and when Redux state changed, all containers will re-render. Putting it another way, any Redux state change will cause all containers to re-render. The component that is child of a container whose prop changed will also re-render as a result. When we use hook as component, the component will re-render if:
- Prop changed
- State from useState changed
- Context value from useContext changed
If you think through this, re-render should work automatically when we move from Redux to Hooks as long as the context value will change if a re-render is required due to something other than prop and state. In Figure 2, we purposefully put all the context state and inApi as value provided by context, therefore guaranteeing re-rendering when any of these changed since there should not be any re-render required if input and state of the context provider didn’t change. Relax knowing that this is already taken care of as long as context contains all the state and inApi from ContextProvider.
Build App around Context
As we move to context, we have t
Answer to Exercises
Exercise 1
Hook is a Function Component only if it returns React.Element. Function component is a hook if it make hook calls such as useState.
Exercise 2
Should you write Class Component if you can solve with Function Component?
No. Function Component is easier to write and can be used as function. Function Component is more versatile and less restrictive.
Should you write hook if you can solve with function?
No. Function is easier to write and can be used as hook. Function is more versation and less restrictive.
Should you write component if you can solve with hook?
No. Hook is easier to write and can be used as component or function. Hook is more versatile (for example, using hook as function avoids creating unnecessary Components that has rendering overhead).