Mutations
Mutations are a first class api of RocketJump to describe asynchronous mutation
of the state.
You can add mutations to your RjObject with the mutations
config option, an object
where the keys are the mutation names and the values are configuration objects.
A mutation is essentially defined by two elements:
- The effect, that follow the same rules of RocketJump effect.
- The logic to update the root state from the effect result (updater).
Then we can add some accessory options:
- The effectCaller for the mutation effect.
- The takeEffect to describe the mutation side effect.
- The reducer to track mutation state.
Mutation are based on RocketJump elements. So for each mutation RocketJump perform the following tasks:
- Create a side effect with the same rules of RocketJump side effects using mutation
settings
effect
,effectCaller
andtakeEffect
. - Add an effect action creator using the mutation name (the key of your configuration).
- If the
reducer
mutation option is given create a reducer under the key:mutations.[mutationName]
. - Apply the
updater
function to your root state when the effect completed with success.
Here an example of a simple mutation:
#
Writing mutationsNow we go deep of how confguring mutations.
effect
#
The effect of the mutation, works as RocketJump effect.
updater
#
A pure function used to update your root state in response of mutation effect completation.
Otherwise you can provide a string refer to an action creator name.
For example we can use the built-in updateData
action creator to simple
update the data
when mutation complete:
reducer
#
Differently from the main effect mutations doens't came with a default reducer
and related state.
You can anyway attach a reducer to a mutation using the reducer
option in
the mutation config.
The main point to note is that when you write a reducer for a mutation
the same action types of standard reducer are dispatched.
Specifically this mean that you can write generic and reusable reducer
for you muation!
For example write a mutation reducer to track a loading/error state at time,
this is true for scenarios like submit a form or when you single interaction
at time:
effectCaller
#
The effect caller of mutation effect, if you don't specify
them is inherit from effectCaller
defined in RocketJump configuration.
If you provide the effectCaller
on mutation config mutation use the effect caller
you provide (also applies to 'configured'
).
If you want not to have any effect caller on a specific mutation you can pass
false
.
takeEffect
#
The (take effect)[side_effects.md] of the mutation side effect, works exactly
as RocketJump main take effect the only difference is that
the default value is 'every
' rather than 'latest'
.
This decision was made because tipically you want track every effect result
of mutations effect instead of the latest.
#
Consume mutation stateAs explained in the other parts of the doc, the default state computed from an RjObject is the root state. So to consume the mutation state you should use computed o rely on selection argument i the consuming api.
An example with computed used the previous ProductDetailState
example:
The same example using selectState
with useRunRj
.
#
Mutations helpersState and updater are handled for you by RocketJumo as you can see, but mutations
are still implemented on top or RocketJump ecosystem, so the action dispacthed
on reducer still available to all reducers.
Most of the time you are good with reducer
and updater
config on mutation
but if you need you can intercept the action dispatched by mutations and do what you want.
To help with this cases RocketJump expose two helpers:
The makeMutationType
function to make an action type for give mutation name:
Usage:
The subType
is usally one of RocketJump core action types such
SUCCESS, FAILURE, PENDING etc..
And the matchMutationType
function:
Usage:
#
Standard mutationsRocketJump provides some utilities to setup sensible defaults on takeEffect and reducer for the most common cases.
#
Single mutationThis options set is thought for mutations that have no overlapping or concurrent runs. A common use case, for instance, is a form submission.
This preset sets takeEffect to exhaust and configures the reducer to mantain a state with the following shape:
You can apply it to a mutation like this
#
Multiple mutationThis option set is designed for mutations that have multiple concurrent runs. Furthermore, it applies a grouping logic: runs belonging to the same group cannot be parallel and only one run per group can be active at a time.
The application of this preset requires the user to define a key derivation function, that is a function that computes a key from the params fed into the mutation call. Runs with the same key are inserted in the same group, with the logic stated above.
This preset sets takeEffect to groupByExhaust, and the reducer is configured to mantain a state with the following shape:
You can apply it to a mutation like this
#
Optimistic mutationsWhen you trigger a mutation you need to wait the effect result to complete to
actually see the changes reflet in your UI.
In some cases you want to optimisic update your state immediatly in response of
user interaction and eventually rollback the update in case of mutation failure,
this is when optimistic mutation come in rescue!
#
When to use optimistic mutationThis is up to the programmer but in general you should use optmistic mutation when you can desume the mutation effect result from its inputs. For example an API that toggle a todo is a good candidate to an optimistic mutation while an API that add a new todo with a new id to your todos is less a good candidate.
#
How to use optimistic mutationTo start using optmistic mutation you should provide the optimisticResult
option to your mutation config.
The optimisticResult
function will be called with your params (as your effect)
and the return value will be passed to the updater to update your root state.
If your mutation SUCCESS RocketJump will commit your state and re-running your updater ussing the effect result as a normal mutation does.
Otherwise if your mutation FAILURE RocketJump roll back your state
and unapply the optimisticResult
.
note
All action dispatched between the run and mutation failure are not lost
are re-applied to your state without the optimistic result.
This is possible cause redcuer are pure functions.
Heres to you a simple optimistic mutation example:
Sometimes you need to distinguish between an optmisitc update and an update from SUCCESS if you provide the optimisticUpdater
key in your mutation config the optimisticUpdater
is used to perform the optmistic update an the updater
to perform the update when commit success.
If your provided ONLY optimisticUpdater
the success commit is skipped and used current root state,
this is useful for response as 204 No Content style where you can ignore the success
and skip an-extra update to your state an save a React render.
#
Write consistent optmistic mutationsSince RocketJump re-apply your actions in case of failure if your effect calculate the response using the remote "state" such as a databse you should prefer to write your update logic in your updater rather then in optimisti result.
Ok, let's clarify the concept with a real example. Imaging having an api
called /increment
that increments a remote counter.
If you write an updater like this:
You call them:
Imaging that you call handleIcrement
three times and the second time it's fail.
When RocketJump re-apply the actions the last action will be called with the
2
value optimistic updater make it 3
and the state it's update with the 3
value.
Now if you move the logic inside the optimisticUpdater
instad:
Now if the second time the increment fails RocketJump re-apply the actions
to your state and you see the correct value of 2
in sync with your server!