The Problem: reuse, but not exactly
One of the recurring themes in building large enterprise applications is doing the same thing, but with minor changes. One UI implementation needs to be “copied”, but there are always minor details preventing 100% reuse.
The data fetching layer is one example of such a detail. On one instance of your UI, you might get your data through a GraphQL library and another instance requires a totally different stack.
No problem, you might say, just have components that take the data as parameter and do the fetching part separately. This is correct, but we’re talking enterprise apps here. You might have complicated app logic baked into React hooks within other hooks, which are then used inside common components.
So how do we isolate and decouple the differing logic within React hooks? Can we achieve some kind of dependency injection with hooks? Yes, we can.
The Problem: Peculiarities of React Hooks
As with any data, functions or components, hooks can also be passed via React props. There is one important gotcha (see Rules of Hooks):
Hooks cannot be called conditionally (unless you wrap them in a component). A component or hook must always call the same number of hooks in each render. So you can’t use this kind of structure:
Hooks can be created dynamically (in a render function), but I strongly discourage this since it’s a recipe for disaster and potential infinite renders.
The Problem: Conventions of hook based libraries
If you are using e.g. React Query for data fetching you need an additional setup. Many libraries require a Provider higher up the component hierarchy before you can use the actual hooks.
Let’s assume we have one custom hook implementation using React Query. What if we need another data fetching method that does not require a provider? How do we decouple the provider part?
The Solution Part 1: Hook Factories
If you have components or hooks which depend on other hooks, it might be useful to use factories. A hook Factory is simply a function that returns a hook.
In this example, our hook useCount is created by calling the factory createCountHook with injected hook useGetDataMock.
The Solution Part 2: Strategy Pattern
If your differing implementation needs more than just single hooks – like a separate Provider or a number of business logic functions – consider the Strategy Pattern.
What you need to do:
- Design your interface. What are the parts that need to be different for each strategy? This can include hooks, logic functions, Providers or other React components.
- Create a Strategy Provider component with a React Context. This will take your current strategy implementation as a parameter.
- If needed, create proxy hooks for actual usage. These hooks get the actual implementation from the Strategy Provider and delegate to them. You would use these proxies in your reusable components.
- Implement your different strategies
- In the actual application, mount the Strategy Provider and provide desired strategy implementation(s) to it
Here’s an example of a Strategy Provider from our demo repository. ContextValue is the interface that is implemented by each strategy.
Our example strategy contains only one hook. In your application you would use this as follows:
With this kind of Strategy Pattern you can effectively decouple your implementations, even if your business logic in encoded in hooks within hooks. It is possible to change the strategy at runtime, or simply use a static strategy if you’re building multiple different applications. None of the unused strategies “pollute” your resulting bundle if you follow common exporting and tree-shaking guidelines.
Demo repository is available at GitHub.