Rematch Code Deep Dive: Part 4 - Third party plugins
This is a part of the Rematch Code Deep Dive Series, focusing on third-party plugins of Rematch.
Unless specified otherwise, the code version in this column is
@rematch/core: 1.4.0
.
In the previous article, I introduced the plugin mechanism of Rematch and its two core plugins. Besides these, the Rematch team has developed several third-party plugins, such as immer, loading, select, persist, updated, etc. In this article, I will introduce two of the most commonly used plugins: immer and loading.
Before explaining them, let’s first review the code structure and components of Rematch:
|
|
As you can see, the two core plugins mentioned above are located in the src
directory, while the third-party plugins are in a separate plugins
directory. In subsequent articles, I will mention that in Rematch v2, we integrated these two core plugins into rematch/core
, so there will no longer be a src/plugins
directory.
Below are the components of Rematch:
1 immer
Let’s start with the immer plugin. It is inspired by immerjs and facilitates creating immutable data in reducers in a mutable way. In simple terms, without immer, the usual practice in a reducer is something like this:
|
|
With immer, we can do:
|
|
Implementing the immer plugin is relatively straightforward. It involves wrapping the normal reducer to process it through immer. Let’s look at the code implementation:
|
|
The plugin exports a plugin.config.redux.combineReducers
configuration, which is merged into rematchConfig.redux.combineReducers
through the mergeConfig()
method in the initialization process and then used when creating the rootReducer.
This combineReducersWithImmer
essentially adds a layer to each modelReducer. The state passed into the modelReducer is actually immer.produce
’s draftState
, so we can directly modify it. If the state is a simple data type, immer.produce
is bypassed.
Regarding immer.produce
and combineReducers
, for more usage and principles, please refer to the official documentation. I won’t repeat them here.
However, there are two flaws in the design of immer:
In the second function parameter of
immer.produce
, it’s not necessary toreturn next
. Immer uses the finaldraftState
to construct a new state. If usingreturn newState
, be careful not to modifydraftState
and then return a new object unrelated todraftState
, as this will result in an error.The redux-related configurations mentioned in plugins are merged into Rematch’s global configuration. However, if two plugins are used and both export this configuration, the latter plugin’s configuration will not be applied. Below is the relevant part of the merge code:
|
|
I always find this part of the design confusing. Many redux-related configurations, if they cannot be saved as an array like the enhancers
and middlewares
above and are replaced instead, means that the configurations defined in our plugins may not be effective. For this reason, in Rematch v2, we implemented a more granular plugin hook called onReducer
, and used it to refactor immer. I will detail this in a later article.
2 loading
Next, let’s look at the loading plugin, which is slightly more complex than immer. Its main usage is the onModel
plugin hook, and it ultimately exports a loading model. The state within this model is used to determine whether asynchronous side effects (e.g., network requests) are in progress.
To make it easier to understand, I’ll explain in three parts: firstly, the initialization code; secondly, the onModel
hook; and finally, the two reducers.
2.1 Initialization
Let’s start with the initialization code:
|
|
We defined an initial state variable cntState
, which consists of three parts: global
to judge whether there are side effects in progress globally, models
to judge whether there are side effects in a specific model (e.g., loading.modelA
), and effects
to judge whether a specific side effect is in progress (e.g., loading.modelA.effectA
).
When initializing, the default modelName is set to loading
, then a converter is defined to convert state between boolean and number types. (Boolean can only indicate whether the side effect is in progress, while number can indicate the number of side effects in progress, with 0 representing none. This design facilitates configuration as needed.) Finally, cntState.global
is set to 0 and converted using the converter.
In the defined loading model, there are also two reducers, which we will explain in the third step.
2.2 onModel hook
Next is the most important part, the usage of the onModel
hook:
|
|
Let’s look at it in three steps:
Firstly, since the model exported by the plugin also goes through the onModel
hook, we first exclude the loading model itself. Then we initialize state.models
and retrieve all actions of the model from this.dispatch
.
Note: As mentioned before, the context
this
in the plugin hooks function is bound to the pluginFactory instance, anddispatch
, as an exported attribute of the dispatch plugin, has also been added to this pluginFactory instance (if you don’t remember, please refer to the previous article).
Secondly, we traverse all actions, processing only effect actions: initializing state.effects[currentModel][effectAction]
, and then filtering out unnecessary effects through a whitelist or blacklist.
Finally, and most importantly, we implement the wrapping of the original effect action. The wrapped action function, before and after the execution of the original effect, as well as in case of an error, calls the reducer of the loading model to manage the state during the execution of the original effect. In the third step, let’s look at these two reducers separately.
2.3 Two reducers
The loading model implements two reducers, hide
and show
:
|
|
Both hide
and show
reducers receive a payload as an object with two properties: modelName and effectName. Through these two properties, a specific effect can be found and its execution status updated. For example, for show
, the counter of the effect in progress is increased by 1, and for hide
, it is decreased by 1. The effects in progress include global effects, model effects, and specific individual effects. Finally, the state is updated and converted through the converter.
3 Summary
In conclusion, that’s all about the Rematch code. In the next two articles, the first one will discuss some design changes from Rematch v1 to v2 and why we made them; the second will focus on TypeScript support in Rematch v2, which is the biggest usage change in the upgrade to v2 and is also my main contribution to the Rematch team. I will discuss how I navigated the Rematch type system and some of the current issues with this system. Stay tuned!