React tricks: Hooks inside Function as a Child Component
Ok, I will try and keep this short.
Actually, I will just give you the solution. If you were trying to use function as a child pattern you can definitely go ahead and try this in case you wanted to include hooks in your function child. Also fair warning this does break the rule of hooks.
The question is should you use it and how did I land upon this solution? For seasoned users of React, this may be pretty obvious at first sight why it is working. For the rest well you can go ahead and give a read.
Sandbox Link: link
During my time at Typito, I did experiments with a lot of weird patterns. One of the patterns that I personally ended up liking was the React functions as a child pattern. Now apparently this is a debated pattern, in the community from what I have read. Some recommend. some do not. Some say pass it as a prop function some say pass it as a child.
However, you want to do it. I find the pattern to be handy for achieving IOC and injecting dependent variables from parents. Especially when I want to follow a pattern where the parent component will inject some values into the child component where only the parent knows what these values are.
Before we even begin I want to emphasize the idea of IOC first.
- Why IOC? To simply put, separation of concern. You do not want a component that evaluates parent container size being tied with its child component. Ideally, you want it to be separate. Often how you evaluate the parent itself can change. Immediate vs Top-level (for different kinds of layout strategies). If we translate this to a design choice, I’d prefer a two-component with distinct strategy names, implementing the same spec of injection of properties.
- Component Tree as Intent? Although React emphasizes composition, I doubt how many times people are able to do it successfully (legacy code and deadlines anyone ?). The IOC mindset where you have an orchestrator and artefact injector forces you to decouple and write components in such a way that your boilerplate and actual business logic are separated out. Basically, it’s a good way to build your platform onto which you can do more stuff while being platform agnostic. With this, anyone reading your component tree is pretty clear with its intent and does not have to dig into the if/else branches of your component.
- Avoiding Prop drill down? Anyone who has worked with large React codebase will know this is a real issue. Especially when you have really complex UI components, doing a lot of fancy stuff. While you want to make things generic and re-usable, the implementation or existing code may not allow for the same. I feel using useContext combined with useReducer for internal state management across the component tree and using useContext for providing context-specific function (black vs white theme for app?) is a handy solution for this. This tends to break through when your injected values are components and need changing based on some state every now and then. You will find it later down in here discussing the same.
Also here are some incoming arguments against this pattern addressed
1. But, but I can do clone inside Parent component
Yes, yes the example I will be showing can be done using cloning. But cloning will require parent to know the argument structure/spec of the child component. This pattern can exclusively help you avoid the same.
2. Why do you not use context?
Well, you can do that. Before we start the opinionated portion, I want everyone to understand so far the examples have focused on a transform function, which in the real world can actually represent a lot of things. It can be a static value, a non-static dynamic function or a Component in itself. This is mentioned so that you don’t start seeing the Redux pattern emerging. Because it is not, the point is that InjectionWrapper is a spec that can be implemented by anyone. This means your UI or the Orchestrator component (GenericComponent) is responsible for actually combining multiple such injected operators, values and components together to get the final result.
This is much different from what Redux is meant for. Although you can still achieve it, you are just adding more bloat to something simple. Plus you may end up storing non-pure values in the Redux state object. Again your choice in this !!
So this works and actually serves the purpose that we wanted to. transform function can even be a component here. The concern is however tied with the issue of re-rendering for updates. So there are two ways you can do it
- Avoid useMemo: Wait why did I use it, to begin with. Well either that or I use a globally defined object with that function. The intent is to keep the reference of the value variable the same across renders. We do not want the context to be pushing new prop values every time leading to unnecessary renders. You can read more about the issue here. You can see the solutions posted and each one of them just tries to avoid the re-render caused by changing context injected values. Can read it here as well.
The dichotomy of situations is if you avoid useMemo, or use it with a dependency list you can update your context but at the cost of large re-renders. - Use an Observable pattern inside a context: This is an idea I have been thinking about for a while. But the solution is more close to an inter-component messaging system than an actual context switcher. There is also the issue of which message channel to subscribe to in the child component. Your context producer is still truly not decoupled from your consumer component, since the consumer needs to know what context to consume. Same code as above, just the updates are more granular and no more plagued by re-render problems. Not a great solution if you ask me. But usable.
The biggest reason I would avoid this is that I feel it would be an overkill of a solution if you just want two levels of components being decoupled from each other.
3. Wait a minute you have Higher-Order Components right?
Yes and this is where it gets interesting. The Function as a Child Component is just a spin on this pattern. From the HOC official doc, one can infer that HOC patterns actually focus on static binding. You can avoid that by using a memo to generate the new component every time props change, or use the same component throughout the process by hoisting it on top of the file. Here is a sample of what it might look like here.
Ok now, this looks like something you may have seen in your codebase in some form. I feel a lot of us have used it already. The only problem however is my readability will suffer as soon as I take away the HOC generator function somewhere else in a different file. Which is generally the case. Plus achieving the dynamic nature of changing context might just require us to recompute the HOC setup code. In this case, it just returns a component. In other cases, there may be other things going on there.
4. Are you not overcomplicating this? Just use an Adapter ?
Yes, we can !! You can actually switch to just creating a single component like above in the Context example which can consume the value from the parent directly, while you make sure to pass a child that can understand the argument structure of the parent and transform and pass it down to the actual child it is trying to wrap. I believe this will definitely work, except this may become an issue for anyone who is using your library. Think how you always used to have that wrapper component in redux to map the state values. Where all you needed was a function to do the mapping.
Just to make sure I have not lost you midway somewhere there I want to summarise the problems now.
- We need a simple solution that works for simple two-level component
- We need IOC but not at the cost of readability
- We need dynamic props, components and everything with the same fluidity a simple component composition in React would offer us
And I think Function as a Child can definitely address these
Function As A Child
Well before you come forward with pitchforks calling it an anti-pattern. Know this I understand you, when I first saw it back in 2020, I was confused and did not like it. But eventually, when I started writing platform level stuff (components that others can use to measure height, achieve a consistent UI, some particular layout), I started to see the benefits of it.
I hope you see my point now, that this pattern improves visibility while reducing boilerplate for simple use cases and all the while keeping our original intention of IOC without having the injector know of the spec. Because anyone using the GenericComponent and the Injector just needs to write the Adapter for the Function Child. The Adapter itself can be a HOC however and can totally be static. Or maybe it gets some of its functions injected in a similar way.
Before I answer my original post point i.e how do I get hooks here? 👀 I want to bring some context as to where I found it useful.
- Measure Components: Imagine a scenario where your UI has to be responsive and you are doing calculations by using browser and parents' dimensions. Question do you want the measuring code to be contained in the same place as your business logic? Perhaps no !! I found it really useful in such components where I could separate out generic platform logic use cases from the core presentation logic. To be honest I actually started of by writing hooks for these but soon realised it just adds unnecessary bloat and wrapped the hook in a component that takes in a function as a child.
- Layout/Behavioural Components: So there have been some use cases where I had components which achieved a specific layout. But the underlying children were always the same mostly. Sometimes these layout components also use something like the above mentioned and used another context/transform injectors👆 to achieve the final data that can be useful to the child to do the final rendering. If you are from the OOPs mindset then this is very similar to inheriting some request handler class and implementing the handler. All dirty work is done by the base class itself all you have to do is just implement the request handler. Clean and Simple, does not mingle platform code with your business logic.
- Capturing Scope: Same advantage as a HOC. I was using it at places and saw that it’s pretty handy
But there have been cases where this kind of component structure started to show its issues. Here is one example of such a case.
The advanced hooks users may have already seen it coming from miles away. If you are not one here is the output on the console when you click the button
Now lets discuss this problem and it’s possible solution
Why does this happen?
You see React in its current iteration past 16, has been maintaining the VDOM in a Fiber architecture. Although I won’t go deep down into it. But to simplify every React node / Component / CreateElement corresponds to one Fiber node. Think of it as a Node in LinkedList. Actually, truth be told that’s what really is when react does the reconciliation. Now each of its nodes has a fixed list of hook type and count being assigned to it on first creation. That is you cannot change the count of hooks used, by doing an early return. In our case given the Function As A Child, is a function and not a component it does not generate a fibre node when being called by the Injector Component. The hooks defined inside the child are actually part of the Fiber corresponding to the Injector Component. Hence when we do a conditional rendering it breaks React. Since the hook type has changed.
Now there are two ways I can see one can address this
- Take the state away and hoist it up in the parent component and pass it down to child function: I don’t like this solution. One it takes up memory for no reason. Remember we are establishing a pattern here. Which will be repeated across the application. This means everyone may just end up declaring a bunch of unneeded hooks taking up more memory. The footprint will be determined by the scale of the application. Two you are breaking the entire idea of IOC here. Doing this makes my parent component has a lot more context for its child than it really needs. We need to decouple them. Remember IOC is not only about code decoupling but also about how much context one needs to achieve a behaviour. If I need a new conditional rendered component I can just add that to the list in App Component and be done. Having the internal state exposed in parent may force me to investigate their usage only to have wasted my time.
- Just create a component from your child's function: I like this one. One it’s less code and two it does not breaks any of our tenets.
So we now have a solution to use Hooks. But it seems it just adds some extra mess. Why not extract these patterns away from a bring back the cleaner App Component.
Drum Roll Please !!!!!
Now I can see you doing this, saying out loud
“Wait is it not very similar to writing a wrapper component and using it as an Adapter to consume the Injector values”
The answer is yes it is. You can achieve this exact thing without a Function As A Child pattern and just rely on a good old Component to act as an adapter between the Generic Injector and the Generic Consumer. I am using Generic to signify that you just know the spec of these components and not the implementation. That means you must provide the Injector with a child having a specific argument structure.
But let’s roll back and see how we even arrived at this solution? FAC was meant to cut the boilerplate and obviously give us IOC, as well as the custom child to render from some other part of the codebase that we don’t know about. Decoupling at its best. The only minute problem that was there, as we could not use hooks inside FAC.
The final solution solved that. The result however did end up looking oddly similar to the good old adapter component solution.
You can also opt for this final solution which does not take props
Why does this work though ?
Well, the useCallback actually returns a function in this case and since use the JSX syntax to treat it as a component we are actually just creating a new React VDOM node i.e new Fiber node. The function as a child basically now becomes the render function of this pseudo component. Which will change every time the child's function itself changes. Which will lead to demounting and remounting of the component. Leading to hooks being removed and added back. Leading to all working as usual.
Conclusion
This post was actually to discuss this trick component. But as you can see like all tricks this one is also just a trick. Again this may or may not be useful to you. In my case, it was actually handy to reduce the boilerplate since I was doing a lot of FAC in places, and I was trying to reduce the amount of hooks in the parent component and decouple stuff.