
Security News
Feross on Risky Business Weekly Podcast: npm’s Ongoing Supply Chain Attacks
Socket CEO Feross Aboukhadijeh joins Risky Business Weekly to unpack recent npm phishing attacks, their limited impact, and the risks if attackers get smarter.
react-rocketjump
Advanced tools
Manage state and side effects like a breeze
React RocketJump is a flexible, customizable, extensible tool to help developers dealing with side effects and asynchronous code in React Applications
Benefits of using React RocketJump
yarn add react-rocketjump
// (1) Import rocketjump (rj for friends)
import { rj } from 'react-rocketjump'
// (2) Create a RocketJump Object
export const TodosState = rj({
// (3) Define your side effect
// (...args) => Promise | Observable
effect: () => fetch(`/api/todos`).then(r => r.json()),
})
// (4) And then use it in your component
import { useRunRj } from 'react-rocketjump'
const TodoList = props => {
// Here we use object destructuring operators to rename actions
// this allows to avoid name clashes and to have more auto documented code
const [{
data: todos, // <-- The result from effect, null at start
pending, // <-- Is effect in pending? false at start
error // <-- The eventually error from side effect, null when side effect starts
}] = useRunRj(TodosState) // Run side effects on mount only
return (
<>
{error && <div>Got some troubles</div>}
{pending && <div>Wait...</div>}
<ul>
{
todos !== null &&
todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}
import { rj } from 'react-rocketjump'
export const TodosState = rj({
effect: (username = 'all') => fetch(`/api/todos/${username}`).then(r => r.json()),
})
import { useRunRj } from 'react-rocketjump'
const TodoList = ({ username }) => {
// Every time the username changes the effect re-run
// the previouse effect will be canceled if in pending
const [
{ data: todos, pending, error },
{
// run the sie effect
run,
// stop the side effect and clear the state
clean,
// stop the side effect
cancel,
}
] = useRunRj(TodosState, [username])
// ...
}
import { rj } from 'react-rocketjump'
export const TodosState = rj({
effect: (username = 'all') => fetch(`/api/todos/${username}`).then(r => r.json()),
})
import { useRunRj, deps } from 'react-rocketjump'
const TodoList = ({ username }) => {
// Every time the username changes the effect re-run
// the previouse effect will be canceled if in pending
const [
{ data: todos, pending, error },
{
// run the sie effect
run,
// stop the side effect and clear the state
clean,
// stop the side effect
cancel,
}
] = useRunRj(TodosState, [
deps.maybe(username) // if username is falsy deps tell useRj to
// don't run your side effects
])
// ...
// there are a lot of cool maybe like monad shortcuts
// use maybeAll to replace y deps array [] with all maybe values
// if username OR group are falsy don't run the side effect
useRunRj(deps.allMaybe(username, group))
// strict check 4 null
useRunRj([deps.maybeNull(username)])
useRunRj(deps.allMaybeNull(username, group))
// shortcut 4 lodash style get
// if user is falsy doesn't run otherwise runs with get(value, path)
useRunRj([deps.maybeGet(user, 'id')])
// ... you can always use the simple maybe to generated custom run
// conditions in a declarative fashion way
useRunRj([
(username && status !== 'banned')
? username // give the username as dep only if username
// is not falsy and the status is not banned ...
: deps.maybe() // otherwise call maybe with nothing
// and nothing is js means undefined so always
// a false maybe
])
}
import { rj } from 'react-rocketjump'
export const TodosState = rj({
effect: (username = 'all') => fetch(`/api/todos/${username}`).then(r => r.json()),
})
import { useEffect } from 'react'
import { useRunRj } from 'react-rocketjump'
const TodoList = ({ username }) => {
// useRj don't auto trigger side effects
// Give you the state and actions generated from the RocketJump Object
// is up to you to trigger sie effect
// useRunRj is implement with useRj and useEffect to call the run action with your deps
const [
{ data: todos, pending, error },
{ run }
] = useRj(TodosState, [username])
useEffect(() => {
if (username) {
run(username)
}
}, [username])
function onTodosReload() {
// or with callbacks
run
// in callbacks is saftly to run side effects or set react state
// because callbacks are automatic unregistred when TodoList unmount
.onSuccess((todos) => {
console.log('Reload Y todos!', todos)
})
.onFailure((error) => {
console.error("Can't reload Y todos sorry...", error)
})
.run()
}
// ...
}
import { rj } from 'react-rocketjump'
export const TodosState = rj({
mutations: {
// Give a name to your mutation
addTodo: {
// Describe the side effect
effect: todo => fetch(`${API_URL}/todos`, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(todo),
}).then(r => r.json()),
// Describe how to update the state in respond of effect success
// (prevState, effectResult) => newState
updater: (state, todo) => ({
...state,
data: state.data.concat(todo),
})
}
},
effect: (username = 'all') => fetch(`/api/todos/${username}`).then(r => r.json()),
})
import { useEffect } from 'react'
import { useRunRj } from 'react-rocketjump'
const TodoList = ({ username }) => {
const [
{ data: todos, pending, error },
{
run,
addTodo, // <-- Match the mutation name
}
] = useRj(TodosState, [username])
// Mutations actions works as run, cancel and clean
// trigger the realted side effects and update the state using give updater
function handleSubmit(values) {
addTodo
.onSuccess((newTodo) => {
console.log('Todo added!', newTodo)
})
.onFailure((error) => {
console.error("Can't add todo sorry...", error)
})
.run(values)
}
// ...
}
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,
optmisticUpdater: (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
.
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.
The full documentation with many examples and detailed information is mantained at
https://inmagik.github.io/react-rocketjump
Be sure to check it out!!
Since v2 rj ships a redux-logger inspired logger designed to run only in DEV and helps you debugging rocketjumps.
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'
})
You can find an example under example, it's a simple REST todo app that uses the great json-server as fake API server.
To run it first clone the repo:
git clone git@github.com:inmagik/react-rocketjump.git
Then run:
yarn install
yarn run-example
3.0.0
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
},
})
makeAction
to makeEffectAction
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 ~~rj.configured()
~~ syntax in favor of simply 'configured'
string
when setting the effectCaller
option.
combineReducers
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,
},
})
addSideEffect
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
concatLatest
and groupByConcatLatest
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 */]
actionMap
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,
})
makeMutationType
matchMutationType
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
plugins/list
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
: When false
don't touch pagination on insertItem
deleteItemTouchPagination
: When false
don't touch pagination on deleteItem
plugins/mutationsPending
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.
FAQs
Rocketjump your react! Manage state and side effects like a breeze
We found that react-rocketjump demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket CEO Feross Aboukhadijeh joins Risky Business Weekly to unpack recent npm phishing attacks, their limited impact, and the risks if attackers get smarter.
Product
Socket’s new Tier 1 Reachability filters out up to 80% of irrelevant CVEs, so security teams can focus on the vulnerabilities that matter.
Research
/Security News
Ongoing npm supply chain attack spreads to DuckDB: multiple packages compromised with the same wallet-drainer malware.