This is a very big release for the Rocketjump library, there are a lot of breaking
changes.
All the library is was rewritten in Typescript and the core (rocketjump-core
package)
it also was rewritten in Typescript and merged back in the codebase.
The philosophy of the library remains the same: Do much with less code.
But we cut some tricky features to take the maxium advantage from Typescript and modern era editors like vscode.
This is a stepping stone relase to future awesome implementation, so some breaking changes are necessary.
On the other hand we notice that simpler apps still working with no additional effort.
In previous version we use a convetion: only the rj()
invocation with
effect signature can create a valid RjObject
that can be used as input for
useRj
or useRunRj
, otherwise the result was a plugin.
In version 3 this convention is removed, instead we introduced an explicit rjPlugin
a function specialized in crafting plugins.
Note that the signature and all the logic realted to the plugin composition remains the same.
In v2:
import { rj } from 'react-rocketjump'
rj(
rj(/* ... */),
rj(/* ... */),
{
effect: /* ... */
}
)
In v3:
import { rj, rjPlugin } from 'react-rocketjump'
rj(
rjPlugin(/* ... */),
rjPlugin(/* ... */),
{
effect: /* ... */
}
)
In previous version the shape of Rocketjump state could change based on the mutations configuration. When the confguration included some mutations state the state shape passes from the one inherit from reducer to:
{
root: /* state from reducer config */,
mutations: /* state from mutations */,
optimisticMutations /* state from optimistic mutation */
}
From v3 we always compose the state using root
key.
Furthermore the state in ALWAYS context is supposed to have this shape.
This means that this don't work anymore:
rj({
selectors: () => ({
total: (state) => (state.data ?? []).reduce((item) => item.price + acc, 0),
}),
})
... But this stil works:
rj({
selectors: ({ getData }) => ({
total: (state) =>
(getData(state) ?? []).reduce((item) => item.price + acc, 0),
}),
})
In previous version computed were ONLY strings and were merged in ambitious way.
In v2 you can write:
rj(
rj({
computed: {
baz: 'getData',
fuzzy: 'isPending',
},
}),
{
computed: {
foo: 'getData',
},
}
)
... And computed state was:
{
foo: any,
fuzzy: boolean
}
In v3 you can specify computed ONLY in rj()
so you can't provide computed
to your plugins.
Sadly this breaks all the default computed in:
plugins/list
plugins/plainList
plugins/map
WHY?
You will be thinking why a breaking changes so destructive was introduced? Beacause in v3 the result state type is mostly infered by Typescript compiler and infering this type of ambitious merging is quite impossible, so we decided to drop it.
In previous version we provide a special '@mutation'
prefix in computed
to select mutation state.
Since v3 we support function as computed so we can simply access the state
related to mutation using a function:
In v2:
rj({
mutations: {
addToCart: rj.mutation.single({
/** **/
}),
},
computed: {
addingToCart: '@mutation.addToCart.pending',
},
})
In v3:
rj({
mutations: {
addToCart: rj.mutation.single({
/** **/
}),
},
computed: {
addingToCart: (state) => state.mutations.addToCart.pending,
},
})
We drop the support for:
rj({
selectors: {
newSelectors: prevSelectors => state => /** **/,
},
actions: {
newAction: prevActions => (...args) => /** **/,
}
})
We only support this syntax:
rj({
selectors: (prevSelectors) => ({
newSelectors: state => /** **/,
}),
actions: (prevActions) => ({
newAction: (...args) => /** **/,
})
})
In previous version the composeReducer
ins't a simple composition utility, but it
merge the inital values of provided composed function, since v3 composeReducer
simply
compose reducers.
In v2:
const { reducer } = rj({
composeReducer: (state = { foo: 23 }) => state,
})
// Root State Shape:
/*{
pending: false,
error: null,
data: null,
foo: 23,
}*/
In v3:
const { reducer } = rj({
composeReducer: (state = { foo: 23 }) => state,
})
// Root State Shape:
/*{
pending: false,
error: null,
data: null,
}*/
You can achieve the same result by doing:
const { reducer } = rj({
composeReducer: (state, action) => {
if (action.type === INIT) {
return { ...state, foo: 23 }
}
return state
},
})
The makeAction
name was too generic and confusing the only reason you have to
use this helper is works with side effect we renamed it to makeEffectAction
.
We improve the rxjs
Side Effect model to make it super powerful.
In previous version all custom effect action are always dispatched to reducer. Es:.
import { rj, makeEffectAction } from 'react-rocketjump'
rj({
actions: () => ({
bu: () => makeEffectAction('BU'),
}),
})
In v2 calling actions.bu()
was supposed to be dispatched in reducer
.
Since v3 you have to manually handle how 'BU'
type side effect is handled.
To simple dispatch it on reducer the code should be something like:
import { rj, makeEffectAction } from 'react-rocketjump'
import { filter } from 'rxjs/operators'
rj({
actions: () => ({
bu: () => makeEffectAction('BU'),
}),
addSideEffect: (actionObservable) =>
actionObservable.pipe(filter((action) => action.type === 'BU')),
})
We also change the custom takeEffect
signature to TakeEffectHanlder
:
interface TakeEffectBag {
effect: EffectFn
getEffectCaller: GetEffectCallerFn
prefix: string
}
interface StateObservable<S = any> extends Observable<S> {
value: S
}
type TakeEffectHanlder = (
actionsObservable: Observable<EffectAction>,
stateObservable: StateObservable,
effectBag: TakeEffectBag,
...extraArgs: any[]
) => Observable<Action>
Another different implementation detail from v2 is that the configured
effect caller
value is no more hold on makeObservable
result instance but is hold directly in a ref
on current dispatched action.
This is an implemntation detail, you shouldn't care if you don't play with rj internals.
This is unlock future implementation when you can use the same makeObservable
value
with different run time effect callers.
We deprectated the syntax in favor of simply rj.configured()
'configured'
string
when setting the effectCaller
option.
A new option combineReducers
can be used in rj()
and rjPlugin()
.
It can be used to provide more reducers along with root
and mutations
reducers.
Is useful to store meta information without touching the root reducer shape:
rj({
combineReducers: {
successCount: (count = 0, action) => {
if (action.type === SUCCESS) {
return count + 1
}
return count
},
},
computed: {
successCount: (s) => s.successCount,
},
})
You can add a side effect in form of Obsevable<Action>
using new
addSideEffect
with the same signature of takeEffect
.
For a real world usage see the WebSocket Example
This standard take effect execute one task at time but if you RUN
a task while another task is excuted it buffer the LAST effect and then excute it.
This is useful in auto save scenarios when a task is spawned very often but you need
to send at server only one task at time to avoid write inconsistences but at the same
time you need ensure last data is sended.
The groupByConcatLatest
is the version with grouping capabilites:
['groupByConcatLatest', action => /* group action */]
This new helper make more simple to build a custom side effect with the
same behaviour of standard effects (run effect with caller dispatch SUCCESS
or FAILURE
).
function actionMap(
action: EffectAction,
effectCall: EffectFn,
getEffectCaller: GetEffectCallerFn,
prefix: string
) : Observable<Action>
To see how to use it see the standar take effects implementation.
The main point of v3 is the ability to inferring the type of RjObject
by your
configuration and plugins.
When using the standard rj constructor rj(...plugins, config)
some stuff can't be
infered Es.. (the type of state in selectors) to avoid bad types in some situation
we give up and we fallback to any
.
We expected that in future version of Typescript we can improve the types experience.
If your are interessed there is an open issue.
Here at InMagik Labs we follow this mantra:
Mater artium necessitas
So to have the maxium from Typescript we introduce the Builder Mode!
When you invoke rj()
or rjPlugin()
you enter the builder mode.
Instead of providing big object of options you chain the same option as builder
and when your are done call .effect({ ... })
on rj()
to build an RjObject
or
.build()
on rjPlugin()
to build a plugin.
Es:.
const p1 = rjPlugin()
.reducer(oldReducer => (state, action) => { /** **/ })
.actions(() => ({
hello: () => ({ type: 'Hello' })
}))
.combineReducers({
plus: () => 88,
})
.build()
const obj = rj()
.plugins(p1)
.selectors(() => ({
getPlus: s => s.plus,
}))
.effect({
effect: myEffect,
})
The makeMutationType
create a mutation action type.
The matchMutationType
match a mutation action type using a flexible syntax.
For more detail to how they works see the: tests
In previous version using the list plugin and calling insertItem
or deleteItem
will trigger this warning:
It seems you are using this plugin on a paginated list. Remember that this plugin is agnostic wrt pagination, and will break it. To suppress this warning, set warnPagination: false in the config object
Since v3 we remove this warning in list plugin and we fix the pagination issue for you by simpy by incrementing / decrementing the count. You can disable this behaviour by passing this new options:
insertItemTouchPagination
: Whenfalse
don't touch pagination oninsertItem
deleteItemTouchPagination
: Whenfalse
don't touch pagination ondeleteItem
This new plugin keep track of multiple mutations peding state.
Expose a selector called anyMutationPending
to grab the related state.
If called without argument track ALL mutations:
import rjMutationsPending from 'react-rocketjump/plugins/mutationsPending'
const maRjState = rj(rjMutationsPending(), {
mutations: {
/** **/
},
computed: {
busy: 'anyMutationPending',
},
})
Accept a configuration object with track
key to specify which mutations
tracks:
const maRjState = rj(
rjMutationsPending({
track: ['one', 'two'],
}),
{
mutations: {
one: {
/** **/
},
two: {
/** **/
},
three: {
/** **/
},
},
computed: {
busy: 'anyMutationPending',
},
}
)
The three
mutation is excluded by tracking.
Fix an issue with rj logger and react fast refresh.
React 17, rewrite tests to support React 17 code is the same of 2.6.0
.
Added .curry(...args)
method to action builder.
Sometimes expecially in custom hooks you need to curry arguments, success,
failures callbacks or metas.
Es:.
function useProduct(id) {
const [data, actions] = useRunRj(ProductState, [id])
const patchCurrentProduct = useMemo(
() => actions.patchProduct.onSuccess(() => alert('Patched!')).curry(id),
[actions, id]
)
// ... ID will be curried and also onSuccess handler
patchCurrentProduct({ price: 33 })
}
.curry
is immutable an create a new builder instance every time is invoked
so is prefer to be used wrapped in a React.useMemo
.
Added optimisticUpdater
and enable auto commit for optimistic mutations.
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 React update to your
state.
If you provide only updater
this is used for BOTH optmistic and non-optimistic
updates as before.
Added optmistic mutations.
To make a mutation optimistic add optimisticResult
to your mutation
config:
rj({
effect: fetchTodosApi,
mutations: {
updateTodo: {
optimisticResult: (todo) => todo,
updater: (state, updatedTodo) => ({
...state,
data: state.data.map((todo) =>
todo.id === updatedTodo.id ? updatedTodo : todo
),
}),
effect: updateTodoApi,
},
toggleTodo: {
optimisticResult: (todo) => ({
...todo,
done: !todo.done,
}),
updater: (state, updatedTodo) => ({
...state,
data: state.data.map((todo) =>
todo.id === updatedTodo.id ? updatedTodo : todo
),
}),
effect: (todo) =>
updateTodoApi({
...todo,
done: !todo.done,
}),
},
incrementTodo: {
optimisticResult: (todo) => todo.id,
updater: (state, todoIdToIncrement) => ({
...state,
data: state.data.map((todo) =>
todo.id === todoIdToIncrement
? {
...todo,
score: todo.score + 1,
}
: todo
),
}),
effect: (todo) => incrementTodoApi(todo.id).then(() => todo.id),
},
},
})
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 state.
If your mutation SUCCESS rocketjump will commit your state and re-running
your updater
ussing the effect result as a normal mutation.
Otherwise if your mutation FAILURE rocketjump roll back your state and
unapply the optimisticResult
.
rocketjump take care of orders of your effects results and the thirdy parts actions dispatched in the meanwhile.
Improved error logging, during effect.
Take this code:
rj({
name: 'MaTodos',
effect: () => 23,
})
Before when this codes runs you see something like this in your console:
What the hell is this? Where come this error? Bad.
Now the same error:
Even better when rj logger is enabled:
No breaking changes, only security fixes
No breaking changes only new features.
Expose a new helper to check if a plain object is a React RjObject
isObjectRj(RjObject) => Boolean
You can use deps
in action creators:
const [data, actions] = useRj(RjObject)
actions.run(deps.maybe(false)) // Don't run
actions.run(deps.maybeGet({ name: 'GioVa' }, 'name')) // Run with 'GioVa'
actions.run(deps.withMeta(23, { id: 23 })) // Run with 23 and meta { id: 23 }
Fixed a bug with useRunRj
and array deps that cause spread array values in effect arguments.
This release improve the support for upcoming React async rendering feature.
All subscription are applied in a saftley way following: https://github.com/facebook/react/tree/master/packages/use-subscription
Furthermore rxjs / side effects logic has been completely rewrited and improved.
There is only a mini breaking change:
If your provided a custom takeEffect to rj()
you have to update
your signature from:
rj({
// ...
takeEffect: (action$, state$, mapActionToObserable, prefix) => {},
})
To:
rj({
// ...
takeEffect: (
action$,
mergeObservable$,
state$,
extraSideEffectObs$,
mapActionToObserable,
prefix
) => {},
})
The other good news from this release are the deps for useRunRj and in general a tools for better handling run with metadata using React hooks.
One of the main goals of rj is to (try) to promoting the functional programming paradigm and most in general a declarative code approach instead of an imperative code approach.
With this in mind we try to make useRunRj
more powerful and declarative.
Ok, the code boy.
Try to imagine a situation when you need to wait a value to run an effect, typically when you have a sequence of rjs or a situation where your value can be valid or maybe can not.
Take this snippet:
function UserProfile({ id }) {
// Fetch the user info \w UserState rj
const [{ data: user }] = useRunRj(UserState, [id])
// Fetch the use company info \w CompanyState
// NOTE this code is broken becose user in null until the
// UserState's effect resolves
const [{ data: company }] = useRunRj(CompanyState, [user.companyId])
}
You need to wait until UserState
's effect resolves before run CompanyState
's effect.
Sure you can simply switch from useRunRj
to useRj
and implement it yourself:
function UserProfile({ id }) {
// Fetch the user info \w UserState rj
const [{ data: user }] = useRunRj(UserState, [id])
// Fetch the use company info \w CompanyState
// This is OK
const [{ data: company }, { run, clean }] = useRj(CompanyState)
useEffect(
() => {
if (user) {
run(user.companyId)
return clean()
}
},
// NOTE run and clean can be ommited because don't changes
// between renders but you can saftley add it to deps
// to make your linter happy
[user]
)
}
Ok, but this code is not too declarative and you need to grab run and clean namespace them if needed and furthermore if the values increase you need to implement more complicated conditions.
Imagine if you can simply tell to useRunRj
:
"please don't run the effect until user
has a value,
when user
is ok then run the effect, thanks."
Now with deps
you can:
// Import the Magik deps from rj
import { deps } from 'react-rocketjump'
function UserProfile({ id }) {
// Fetch the user info \w UserState rj
const [{ data: user }] = useRunRj(UserState, [id])
// Fetch the use company info \w CompanyState
const [{ data: company }] = useRunRj(
CompanyState,
// When user is not falsy run CompanyState
// with user.companyId as param
[deps.maybeGet(user, 'companyId')]
)
}
There are a set functions to help you run effects maybe.
Simply maybe
:
function UserProfile({ id }) {
// Fetch the user info \w UserState rj only when id is not falsy
const [{ data: user }] = useRunRj(UserState, [deps.maybe(id)])
}
Strict null with maybeNull
:
function UserProfile({ id }) {
// Fetch the user info \w UserState rj only when id is not null
const [{ data: user }] = useRunRj(UserState, [deps.maybeNull(id)])
}
Apply maybe check to all deps with allMaybe
:
function UserProfile({ id, role }) {
// If id OR role are falsy don't run effect
const [{ data: user }] = useRunRj(UserState, deps.allMaybe(id, role))
}
Apply maybe strict null check to all deps with allMaybeNull
:
function UserProfile({ id, role }) {
// If id OR role are null don't run effect
const [{ data: user }] = useRunRj(UserState, deps.allMaybeNull(id, role))
}
With deps
you can also declarative set meta.
Set meta on values changes:
function Products({ idStock }) {
const [search, setSearch] = useState('')
const [{ data: user }] = useRunRj(ProductsState, [
idStock,
// run with meta debounced true only when search changes
// (the first run all values changes)
deps.withMeta(search, { debounced: true }),
])
}
Set a set of meta on mount only:
function Products({ idStock }) {
const [search, setSearch] = useState('')
const [{ data: user }] = useRunRj(ProductsState, [
idStock,
// run with meta debounced true only when search changes
// (the first run all values changes)
deps.withMeta(search, { debounced: true }),
// on mount debounced is false
deps.withMetaOnMount({ debounced: false }),
])
}
Set a set of meta on always:
function Products({ idStock, trackId }) {
const [search, setSearch] = useState('')
const [{ data: user }] = useRunRj(ProductsState, [
idStock,
// run with meta debounced true only when search changes
// (the first run all values changes)
deps.withMeta(search, { debounced: true }),
// on mount debounced is false
deps.withMetaOnMount({ debounced: false }),
// trackId always applied
deps.withAlwaysMeta({ trackId }),
])
}
You can combine deps
:
function Products({ idStock, trackId }) {
const [{ data: user }] = useRunRj(ProductsState,
// if idStock is falsy don't run effect
// else run effect with idStock meta
[deps.withMeta(deps.maybe(idStock), { trackId })],
// OR
[deps.maybe(idStock).withMeta({ trackId })]
// OR
[deps.maybe(deps.withMeta(idStock, { trackId }))]
// OR
deps.allMaybe(deps.withMeta(idStock, { trackId }))
)
}
To help you deal with complex condition or edge cases useRunRj
add
a special action to hack the next meta:
function Products() {
const [search, setSearch] = useState('')
const [{ data: user }, { withNextMeta }] = useRunRj(ProductsState, [
// run with meta debounced true only when search changes
// (the first run all values changes)
deps.withMeta(search, { debounced: true }),
// on mount debounced is false
deps.withMetaOnMount({ debounced: false }),
])
// When user type in and setSearch in triggered
// aplly run debounced ... when he user click clearSearch
// button apply a non debounced run
function clearSearch() {
withNextMeta({ debounced: false })
setSearch('')
}
}
This release does not contain breaking changes, but it introduces a lot of new features, awesome stuff, some performance improvements and it improves the rj stability.
The main great feature of rj v2 is the support for mutations.
Mutations add an additional configuration option to the rj object called mutations
(only allowed in the last config object, as it follows the same rules of the effect
configuration option), where you can configure your mutation behaviours.
What is a mutation?
A mutation is simply an effect that, when run with a successful outcome, updates the root rj state with its result.
Ok, for example take a normal rj to fetch some todos from an api.
const MaTodosState = rj({
effect: () => fetch(`/todos`).then((r) => r.json()),
})
Now if you want to toggle your todo with using some api like PATCH /todos/${id}
, you can write a mutation:
const MaTodosState = rj({
mutations: {
toggleTodo: {
// The effect to perform, it accepts the same values of rj effect () => Promise|Observable
effect: (todo) =>
fetch(`/todos/${todo.id}`, {
method: 'PATCH',
body: { done: !todo.done },
}).then((r) => r.json()),
// A PURE function to update the main rj state called when the effect resolves|complete.
// (prevState, effectResult) => nextState
updater: (state, updatedTodo) => ({
...state,
data: state.data.map((todo) =>
todo.id === updatedTodo ? updatedTodo : todo
),
}),
},
},
effect: () => fetch(`/todos`).then((r) => r.json()),
})
Yeah you have written your first mutation!
Ok, but how can I use mutations?
For every configured mutation rj crafts an action creator, using the keys in the mutations
object as names, to the action creators bag.
These action creators trigger the effect
defined in the corresponding mutation and when the effect succedes they use the corresponding updater
to update the (parent) state.
Mutations action creators are effect actions and can be invoked with the Builder as well.
Mutations action creators are supported for all the React bindings useRj
, useRunRj
and connectRj
.
If the mutation name overwrites a preexisting action creator rj prints a warn in DEV.
So for example this is a dummy react component that can be used to toggle some todos using our RjObject:
import React from 'react'
import { useRunRj } from 'react-rocketjump'
import { MaTodosState } from './localstate'
function MaTodos() {
const [{ data: todos }, { toggleTodo }] = useRunRj(MaTodosState)
return (
<ul>
{todos &&
todos.map((todo) => (
<li
key={todo.id}
onClick={() => {
toggleTodo(todo)
// or
toggleTodo
.onSuccess(() => {})
.onFailure(() => {})
.withMeta({})
.run(todo)
}}
>
{todo.title} {todo.done ? '√' : ''}
</li>
))}
</ul>
)
}
The updater can also be a function (with the standard reducer signature (state, action) => state
), or a string that must be the name of an action creator, which will be used to update the state. All the rj actions, even those added by plugins, are valid for this sake.
const MaTodosState = rj(rjPlainList(), {
mutations: {
toggleTodo: {
effect: (todo) =>
fetch(`/todos/${todo.id}`, {
method: 'PATCH',
body: { done: !todo.done },
}).then((r) => r.json()),
// Update the state using the login from insertItem(effectResult)
updater: 'insertItem',
},
},
effect: () => fetch(`/todos`).then((r) => r.json()),
})
To help you write less code we introduced a new standard action creator updateData
that simply updates data of your rj state (this update is done by overwriting the data
prop of the state with the payload of the action).
This isn't an effect action (and hence it cannot be used with the Builder).
With updateData
mutation's code usually becomes more compact, expecially when you deal with REST APIs:
const MaTodosState = rj({
mutations: {
updateUserProfile: {
effect: (newProfile) =>
fetch(`/user`, {
method: 'PATCH',
body: newProfile,
}).then((r) => r.json()),
// Update the state using the login from insertItem(effectResult)
updater: 'updateData',
},
},
effect: () => fetch(`/user`).then((r) => r.json()),
})
You can change the default side effect model with the same logic as the main rj effect:
https://inmagik.github.io/react-rocketjump/docs/api_rj#takeeffect
The default side effect model applied is every
, but you can change it as you wish, for example you can write something like:
const MaTodosState = rj({
mutations: {
updateUserProfile: {
effect: (newProfile) =>
fetch(`/user`, {
method: 'PATCH',
body: newProfile,
}).then((r) => r.json()),
updater: 'updateData',
// ignore all the updateUserProfile() while effect is in peding
takeEffect: 'exhaust',
},
},
effect: () => fetch(`/user`).then((r) => r.json()),
})
You can specify an effectCaller
for your mutation, and you can use rj.configured()
as well.
If your main config has an effectCaller
configured any mutation uses it unnless an effectCaller
is specified in the mutation config. This last effect caller can be any valid effectCaller
or false
(this tells rj not to use any effect caller)
const MaTodosState = rj({
mutations: {
// mutation1 use rj.configured() as effect caller
mutation1: {
effect,
updater,
},
// mutation2 use myAwesomeCaller as effect caller
mutation2: {
effect,
updater,
effectCaller: myAwesomeCaller,
},
// mutation3 don't use any effect caller
mutation3: {
effect,
updater,
effectCaller: false,
},
},
effectCaller: rj.configured(),
effect: () => fetch(`/user`).then((r) => r.json()),
})
Mutations don't have a state by default but sometimes it is useful to track the mutation state, for example to show an indicator while saving or displaying the error message (when some error occures).
When you specify a reducer
in the mutation config you enable the mutation state, your reducer is supposed to handle the standard rj actions:
import { INIT, RUN, PENDING, SUCCESS, FAILURE } from 'react-rocketjump'
The actions dispatched to your reducer are only related to your mutation.
Take this snippet:
const MaTodosState = rj({
mutations: {
mutation1: {
effect,
updater,
reducer: reducer1,
},
mutation2: {
effect,
updater,
reducer: reducer2,
},
},
effect,
})
The reducer1
responds only to the actions generated from mutation1()
, and the same holds for reducer2
with respect to mutation2()
.
Rocketjump automatically namespaces the actions for you to avoid name clashes, so you have only to responde to the standard rj actions and rj will do the rest.
Mutations actions have the standard rj shape, with the bonus that params are by default added to your action metadata:
{
type: INIT
}
{
type: RUN,
payload: {
params, // the params with which your side effect was invoked
}
meta: {
params,
},
}
{
type: PENDING,
meta: {
params,
},
}
{
type: SUCCESS,
meta: {
params,
},
payload: {
data, // the result of your mutation side effect
params,
}
}
{
type: FAILURE,
meta: {
params,
},
payload // the rejection of your side effect
}
When you enable mutations for a rj object your state is sliced in two parts: the mutation's and the root's state.
To select the root state, the usual rj state of v1.x, you have a special selector getRoot
:
const [state, actions] = useRunRj(
MaRjObject,
(state, { getRoot, getData }) => ({
maDataKey: getData(getRoot(state)),
})
)
To select a specific mutation state you have another special selector getMutation
:
const [state, actions] = useRunRj(MaRjObject, (state, { getMutation }) => ({
pending: getMutation(
state,
'mutationKey.path.to.your.state.deep.as.you.want'
),
}))
When you enable mutations state the computed
configuration options for v1.x computed properties will still work as expected.
This works as well:
const MaTodosState = rj({
mutations: {
updateUserProfile:{
effect: newProfile => fetch(`/user`, {
method: 'PATCH',
body: newProfile,
}).then(r => r.json()),
updater: 'updateData',
reducer: ({ pending: false }, action) => /* reducer logic */,
// ignore all the updateUserProfile() while effect is in peding
takeEffect: 'exhaust',
}
},
effect: () => fetch(`/user`).then(r => r.json()),
computed: {
todos: 'getData', // <-- Select data from your root state
},
})
To involve a mutation state in some computed property you can use the special key @mutation
followed by the path of your mutation (in the traditional lodash format):
const MaTodosState = rj({
mutations: {
updateUserProfile:{
effect: newProfile => fetch(`/user`, {
method: 'PATCH',
body: newProfile,
}).then(r => r.json()),
updater: 'updateData',
reducer: ({ pending: false }, action) => /* reducer logic */,
takeEffect: 'exhaust',
}
},
effect: () => fetch(`/user`).then(r => r.json()),
computed: {
todos: 'getData', // <-- Select data from your root state,
updatingProfile: '@mutation.updateUserProfile.pending',
},
})
Rj provides you some standard mutation wrappers, each of which simply injects some defaults.
The rj single mutation is mutation wrapper designed for a mutation that should have no overlapping runs, for example a form submission.
The default takeEffect
is exhaust
, and reducer
is configured to handle a single loading/failure state with this shape:
{
pending: Boolean, // <-- Is my mutation effect in pending?
error: null|Error, // <-- Last error from effect cleaned on every run.
}
To use the single mutation wrapper:
const MaTodosState = rj({
mutations: {
updateUserProfile: rj.mutation.single({
// takeEffect and reducer are injecte for you
effect: (newProfile) =>
fetch(`/user`, {
method: 'PATCH',
body: newProfile,
}).then((r) => r.json()),
updater: 'updateData',
}),
},
effect: () => fetch(`/user`).then((r) => r.json()),
computed: {
todos: 'getData',
updatingProfile: '@mutation.updateUserProfile.pending',
updateProfileError: '@mutation.updateUserProfile.error',
},
})
The rj multi mutation is designed for a mutation that may run multiple times in parallel, based on a "key based" logic: each run is identified by a key and if another effect with the same key is running the new run will be discarded.
The first argument of rj.mutation.multi
is a function that return this key from your params:
;(...params) => key
This is accomplished by using groupByExhaust
as the default takeEffect
for rj.mutation.multi
that groups your effect using the key derived from arguments, effects with the same keys are executed one at time, if an effect with a given key is in place the subsequent run that triggers the same key is ignored.
The default reducer
handle multiple failures and loading states at time with the following shape:
{
pendings: {
[key]: true|undefined,
},
errors: {
[key]: Error|undefined
}
}
To use the multi mutation wrapper:
const MaTodosState = rj(rjPlainList(), {
mutations: {
toggleTodo: rj.mutation.multi(
(todo) => todo.id, // Make a string key from params
{
effect: (todo) =>
fetch(`/todos/${todo.id}`, {
method: 'PATCH',
body: { done: !todo.done },
}).then((r) => r.json()),
updater: 'updateItem',
}
),
},
effect: () => fetch(`/todos`).then((r) => r.json()),
computed: {
todos: 'getData',
savingTodos: '@mutations.toggleTodo.pendings',
},
})
Yess, a redux-logger inspired logger for rj.
Why a logger? Because as rj powered applications grow up in size, it becomes very difficult to track and debug all the effects and the rjs state updates.
This, and some of us was missing a tools like redux logger or redux dev tools.
Yess, yess, yess the React dev tools, especially the new version, is AWESOME but this logger helps you understanding if your rj configuration and effects flows work as you want.
This is what it looks like:
To enable it, just add this snippet to your index.js
:
import rjLogger from 'react-rocketjump/logger'
// The logger don't log in PRODUCTION
// (place this before ReactDOM.render)
rjLogger()
To add a name to your RocketJump Object in the logger output simply add a name
key in your rj config:
const TomatoesState = rj({
effect,
// ... rj config ...
name: 'Tomatoes',
})
Changed how <ConfigureRj />
works.
Up to previous versions effectCaller
in <ConfigureRj />
was replacing the rj effectCaller
unless you explicitly defined them in rj configuration.
This behavior did not provide enough flexibility, so we introduced the ability of rj configuration to be lazy, up to now we apply it only to effectCaller
config option.
Now when you define the effectcaller
in <ConfigureRj />
you enable a lazy configuration value.
Later when you define your rjs you can refer to them in your configuration, using the speical syntax rj.configured()
.
When rj engine encounters the special rj.configured()
slot, the configuration option will become lazy and the recursion will complete when rj mounts and has then access to <ConfigureRj />
context.
Thanks to this you can place your configured effectCaller
where do you want in the recursion chain:
<ConfigureRj effectCaller={myCustomEffectCaller}>
/* This is the scope where the lazy effect caller is enabled */
</ConfigureRj>
rj(
{
effectCaller: myEffectCallerA,
// ... rj config ...
}
{
effectCaller: rj.configured(),
// ... rj config ...
},
{
effectCaller: myEffectCallerB,
// ... rj config ...
}
)
Removed mapActions
from useRj
(was the last argument).
To rename actions simply use object deconstructing, from:
const [state, { fetchStuff } = useRj(
MaRjState,
undefined,
actions => ({ fetchStuff: actions.run })
)
To:
const [state, { run: fetchStuff } = useRj(MaRjState)
For a deep discussion of why this option was removed see: #12
Use a rocketjump object and run it using useEffect
according to deps
, all deps
are passed to run
function.
This is a simple syntactic sugar over useRj
, you can implement it by yourself:
https://inmagik.github.io/react-rocketjump/docs/tips_and_tricks
If you have a rocketjump
with easy-trigger-deps you can use useRunRj
to write less code.
These pieces of codes do the same.
Without useRunRj()
:
const [{ data: product }, { run: fetchProduct, clean: cleanProduct }] = useRj(
MaRjState
)
useEffect(() => {
fetchProduct(productId)
return () => {
cleanProduct()
}
}, [fetchProduct, cleanProduct, productId])
With useRunRj
:
const [{ data: product }] = useRunRj(
MaRjState,
[productId], // <- Deps
true // <- Should clean on new effect? default to true
)
You can find documentation about useRunRj
here: https://inmagik.github.io/react-rocketjump/docs/connect_userunrj
Now rj
has a new config option: computed
.
computed
is expected to be an object that maps from a computed property name to a selector name.
When a rj
in the recursion chain "enables" computed
, the state returned from useRj
or connectRj
is computed according to this configuration, otherwise the default structure is returned. computed
declarations are merged using the normal rj
recursion order.
The computed
mapping is unique, so you can't bind a selector multiple times. If you do this, the last bindings wins.
Example:
const MaRjState = rj(
rj(
{
computed: {
secret: 'getSecret',
ohShit: 'getError',
},
selectors: () => ({
getSecret: () => 23,
}),
},
{
effect: myEffect,
computed: {
todos: 'getData',
error: 'getError',
},
}
)
)
const [state, actions] = useRj(MaRjState)
The value of state is:
{
error: null, // state.error
secret: 23,
todos: null, // state.data
}
Now the plugin list use computed
to avoid mapping the same state over and over again.
A new selector getPagination
is exposed, returning an Object with all the pagination info.
The computed config privided by the plugin is:
(you can completely change this config writing your own computed
config):
{
error: 'getError',
loading: 'isLoading',
list: 'getList',
pagination: 'getPagination',
}