React is just a view library
I've been thinking this a lot recently. React is just a view library.
What it means is that you give it some data, you'll get some UI. Just think of it as a math function of view.
Given the same root state, it returns the same UI.
Given a different root state, it might return a different UI.
However, that is only on the surface level.
In React, by default, the above rule is only applicable to the root function. That means if the root function decides to render, the subordinates will render unconditionally.
This is because React supports Context
. Context
is the only mechanism that allows the state to be passed without being in an argument to the UI function. It is an exception to the functional programming rule that forfeits the benefits of being pure for the entire view library.
Obviously, the purpose of Context
is to ease the data passing in a complex application. Basically trade the performance and simplicity for developer experience. Ok, that is fine. We'll come back to that later.
Besides that exception, we have a couple of interesting hooks to talk about.
Let's start with useState
. It makes sense to have some "persistent" data bound to a UI node. So when UI is rendered, it's local state is not wiped out. This is crucial to build interactive UI. That's fine.
Next, let's talk about useMemo
. This hook is used to create a computed value and cache it based on data dependency. Hmm. It makes sense to hook something based on the internal state of the UI function. But if the dependency all comes from props or context, it makes much more sense to move the computed value close to where those data are.
Next, useCallback
. We know that a function defined inside the UI function can create a closure when referencing the UI's local variable. The local variable can be outdated when UI is re-rendered. So we need to re-create the callback. We can re-create it in every render. Then why do we need this hook? Similar to useMemo
, this hook is to create a computed function based on some data dependency. It only makes sense if the data dependency involves some local state, otherwise, we should move the function entirely closer to the depended data.
However, think about it. If the computed things
only depend on local states, then you won't need the hook at all, because the function or the value will be recreated anyway during the re-render.
So the only use case for the useCallback
and useMemo
should be that it depends on a mix of external state and internal state.
Next, useEffect
. It is to run some asynchronous function when some data is changed. Again, the data dependency needs to be a mix of external data and internal data. Similar to its siblings.
So the existence of the above hooks is to solve the various use cases that have a dependency of the mix of local and non-local data.
What if we can get rid of that?
Can we externalize all the data dependency?
I think that is almost possible.
For local states, if we can keep it minimal and only store UI-related states. Then we can derive app state based only on the UI state either by triggering an action or changing it directly. Then when some app state is changed, we can re-compute the computed values. We wouldn't have the use case of useCallback
anymore as our functions can always get the data from the same source object, not the closure. We most likely won't have the need for effect as well. Since the effect is essentially the actions performed in other actions.
Right, I'm talking about the observable patterns I wrote in my previous post.
Even better, if we can adopt data provider patterns using Consumer only. Then we can achieve UI function components that includes only useState
if needed at all.
This makes the component extremely easy to reason about and easy for testing.
React is just a view library. We should let it do UI stuff only. For data, we can do it separately. That separation of concern is the key to scalable FE apps.