effects
This package is inspired by Sagas and gives you advanced effect management solutions.
included in @reatom/framework
First of all you should know that many effects and async (reatom/async + reatom/hooks) logic uses AbortController under the hood and if some of the controller aborted all nested effects will aborted too! It is a powerful feature for managing async logic which allows you to easily write concurrent logic, like with redux-saga or rxjs, but with the simpler API.
Before we start, you could find a lot of useful helpers to manage aborts in reatom/utils
The differences between Redux-Saga and Reatom.
- Sagas
take
is liketake
+await
. - Sagas
takeMaybe
- is liketake
WITHOUTawait
. - Sagas
takeEvery
- is likeanAtom.onChange
/anAction.onCall
. - Sagas
takeLatest
- is likeanAtom.onChange
/anAction.onCall
+reatomAsync().pipe(withAbort({ strategy: 'last-in-win' }))
. - Sagas
takeLeading
- is likeanAtom.onChange
+reatomAsync().pipe(withAbort({ strategy: 'first-in-win' }))
. - Sagas
call
is a regular function call with a context +await
. - Sagas
fork
is a regular function call with a context WITHOUTawait
. - Sagas
spawn
have no analogy in Reatom. It should create a context without parent context abort propagation. Work in progress. - Sagas
join
- is justawait
in Reatom. - Sagas
cancel
have no analogy in Reatom. It probably should looks likegetTopController(ctx.cause).abort()
. - Sagas
cancelled
- is likeonCtxAbort
.
Two important notes.
- Abortable context in Reatom currently works (starts) only by
reatomAsync
andonConnect
. We will add a new general primitive for that in this package in the nearest time. - A sagas reacts to a [deep] child’s failure, which Reatom doesn’t do. Built-in transaction primitive in a plan.
API
take
Allow you to wait an atom update.
import { take } from '@reatom/effects'
const currentCount = ctx.get(countAtom)
const nextCount = await take(ctx, countAtom)
You could await actions too!
// ~/features/someForm.ts
import { take } from '@reatom/effects'
import { onConnect } from '@reatom/hooks'
import { historyAtom } from '@reatom/npm-history'
import { confirmModalAtom } from '~/features/modal'
// some model logic, doesn't matter
export const formAtom = reatomForm(/* ... */)
onConnect(form, (ctx) => {
// "history" docs: https://github.com/remix-run/history/blob/main/docs/blocking-transitions.md
const unblock = historyAtom.block(ctx, async ({ retry }) => {
if (!ctx.get(formAtom).isSubmitted && !ctx.get(confirmModalAtom).opened) {
confirmModalAtom.open(ctx, 'Are you sure want to leave?')
const confirmed = await take(ctx, confirmModalAtom.close)
if (confirmed) {
unblock()
retry()
}
}
})
})
takeNested
Allow you to wait all dependent effects, event if they was called in the nested async effect.
For example, we have a routing logic for SSR.
// ~/features/some.ts
import { historyAtom } from '@reatom/npm-history'
historyAtom.locationAtom.onChange((ctx, location) => {
if (location.pathname === '/some') {
fetchSomeData(ctx, location.search)
}
})
How to track fetchSomeData
call? We could use takeNested
for this.
// SSR prerender
await takeNested(ctx, (trackedCtx) => {
historyAtom.push(trackedCtx, req.url)
})
render()
You could pass an arguments in the rest params of takeNested
function to pass it to the effect.
await takeNested(ctx, historyAtom.push, req.url)
render()
onCtxAbort
Handle an abort signal from a cause stack. For example, if you want to separate a task from the body of the concurrent handler, you can do it without explicit abort management; all tasks are carried out on top of ctx
.
import { action } from '@reatom/core'
import { reatomAsync, withAbort } from '@reatom/async'
import { onCtxAbort } from '@reatom/effects'
const doLongImportantAsyncWork = action((ctx) =>
ctx.schedule(() => {
const timeoutId = setTimeout(() => {
/* ... */
})
onCtxAbort(ctx, () => clearTimeout(timeoutId))
}),
)
export const handleImportantWork = reatomAsync((ctx) => {
/* ... */
doLongImportantAsyncWork(ctx)
/* ... */
}).pipe(withAbort())