Skip to content
Reatom

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 like take + await.
  • Sagas takeMaybe - is like take WITHOUT await.
  • Sagas takeEvery - is like anAtom.onChange / anAction.onCall.
  • Sagas takeLatest - is like anAtom.onChange / anAction.onCall + reatomAsync().pipe(withAbort({ strategy: 'last-in-win' })).
  • Sagas takeLeading - is like anAtom.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 WITHOUT await.
  • Sagas spawn have no analogy in Reatom. It should create a context without parent context abort propagation. Work in progress.
  • Sagas join - is just await in Reatom.
  • Sagas cancel have no analogy in Reatom. It probably should looks like getTopController(ctx).abort().
  • Sagas cancelled - is like onCtxAbort.

Two important notes.

  1. Abortable context in Reatom currently works (starts) only by reatomAsync and onConnect. We will add a new general primitive for that in this package in the nearest time.
  2. 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())