lens
A set of operators to transform actions payload or an atoms state in a FRP style. Simply put, this package is convenient for reactive processing of actions and effects. But some operators could be useful for data processing too, like filter
, delay
(debounce
, throttle
) and sample
.
included in @reatom/framework
mapState
Simple map utility, which allow you to receive previous dependency state by a second optional argument.
import { mapState } from '@reatom/lens'
// this is a typical code which have a problem with extra updates// in case when an element of the list changes not `myProp`export const filteredListAtom = atom((ctx) => ctx.spy(listAtom).map((obj) => obj.myProp),)// `mapState` could help to solve this problem, as it pass previous state as a second argumentexport const bAtom = listAtom.pipe( mapState((ctx, list, prevState) => { const newState = list.map((obj) => obj.myProp) return isShallowEqual(newState, prevState) ? prevState : newState }),)
filter
Sometimes you already have filteredListAtom
from the previous example and it have no internal memoization. So you could use filter
operator to prevent extra updates.
Updates filtered by comparator function, which should return true
, if new state should continue to propagate. It uses isShallowEqual
from utils package by default.
import { filter } from '@reatom/lens'import { isShallowEqual } from '@reatom/utils'
export const listMemoAtom = filteredListAtom.pipe(filter())// equals toexport const listMemoAtom = filteredListAtom.pipe( filter((ctx, next, prev) => !isShallowEqual(next, prev)),)
This operator could filter actions too!
import { filter } from '@reatom/lens'
export const linkClicked = onDocumentClick.pipe( filter((ctx, event) => event.target.tagName === 'A'),)
mapPayload
Map payload of each action call. Resulted action is not callable.
import { mapPayload } from '@reatom/lens'
export const changeFullname = changeName.pipe( mapPayload((ctx, { firstName, lastName }) => `${firstName} ${lastName}`),)
You could pass initial state by first argument to create an atom.
import { action } from '@reatom/core'import { mapPayload } from '@reatom/lens'
export const onInput = action('onInput')export const inputAtom = onInput.pipe( mapPayload('', (ctx, event) => event.currentTarget.value, 'inputAtom'),)
mapPayloadAwaited
Map fulfilled value of async action call. Resulted action is not callable.
import { mapPayloadAwaited } from '@reatom/lens'
export const newData = fetchData.pipe(mapPayloadAwaited())// OR pick needed valueexport const newData = fetchData.pipe( mapPayloadAwaited((ctx, response) => response.data),)
You could pass initial state by first argument to create an atom.
import { mapPayloadAwaited } from '@reatom/lens'
export const dataAtom = fetchList.pipe( [], mapPayloadAwaited((ctx, response) => response.data), 'dataAtom',)
mapInput
Create action which map input to passed action / atom.
import { atom } from '@reatom/core'import { mapInput } from '@reatom/lens'
export const inputAtom = atom('', 'inputAtom')export const changeInput = inputAtom.pipe( mapInput((ctx, event) => event.currentTarget.value, 'changeInput'),)
debounce
Delay updates by timeout.
import { action } from '@reatom/core'import { debounce, mapPayload } from '@reatom/lens'
export const startAnimation = action()export const endAnimation = startAnimation.pipe(debounce(250))
sample
Delay updates until other atom update / action call.
This code is taken from this example.
import { mapPayload, sample } from '@reatom/lens'
export const lastRequestTimeAtom = fetchData.pipe( mapPayload(0, () => Date.now(), 'fetchStartAtom'), sample(fetchData.onSettle), mapState((ctx, start) => start && Date.now() - start, 'lastRequestTimeAtom'),)
toAtom
Convert an action to atom with optional init state.
import { mapPayloadAwaited, toAtom } from '@reatom/lens'
export const dataAtom = fetchData.pipe(mapPayloadAwaited(), toAtom([]))
plain
Removes all extra properties, useful for exports cleaning.
import { plain } from '@reatom/lens'
const _fetchData = reatomFetch('...')
// ... some module logic with `_fetchData.retry` etc
// allow external modules only fetch data and not manage it by other waysexport const fetchData = _fetchData.pipe(plain)
readonly
Removes all callable signature, useful for exports cleaning.
import { readonly } from '@reatom/lens'
const _countAtom = atom(0)export const changeCount = action((ctx) => { // the module extra logic here _countAtom(ctx)})
// disallow atom to be mutated outside the moduleexport const countAtom = _countAtom.pipe(readonly)
parseAtoms
Recursively unwrap all atoms in an atomized structure. Useful for making snapshots of reactive state. Uses ctx.spy
if it’s available.
parseAtoms
: persistence example
https://codesandbox.io/s/reatom-react-atomization-k39vrs?file=/src/model.ts
import { action, atom, Action, AtomMut } from '@reatom/core'import { withLocalStorage } from '@reatom/persist-web-storage'import { parseAtoms, ParseAtoms } from '@reatom/lens'
export type Field = { id: number name: string value: AtomMut<string> remove: Action}
const getField = (id: number, name: string, value: string): Field => { return { id, name, value: atom(value), remove: action((ctx) => { // ... }), }}
export const listAtom = atom<Array<Field>>([], 'listAtom').pipe( withLocalStorage({ toSnapshot: (state) => parseAtoms(state), fromSnapshot: (snapshot: any) => getField(snapshot.id, snapshot.name, snapshot.value), }),)
parseAtoms
: shortcut example
You can use parseAtoms
to reduce the amount of . Let’s suppose you have the following structure:
interface User { name: AtomMut<string> bio: AtomMut<string> website: AtomMut<string> address: AtomMut<string>}
And use it like this:
import { useAtom } from '@reatom/npm-react'
export const User = ({ user }: { user: User }) => { const [name] = useAtom(user.name) const [bio] = useAtom(user.bio) const [website] = useAtom(user.website) const [address] = useAtom(user.address)
return <form>...</form>}
With parseAtoms
you can refactor usage to look like this:
import { parseAtoms } from '@reatom/lens'import { useAtom, useAction } from '@reatom/npm-react'
export const User = ({ user }: { user: User }) => { const [ { name, // bio, website, address, }, ] = useAtom((ctx) => parseAtoms(ctx, user))
return <form>...</form>}
match
Creates an atom that depending on some condition or data patterns, which can be an atom too. Useful for describing UIs with @reatom/jsx
or any other renderers. Here is the example of routing description from the base template (Vite, TypeScript, React, Reatom).
export const routes = match(isLoggedAtom) .default(() => <Auth />) .truthy( match((ctx) => ctx.spy(urlAtom).pathname) .is('/me', () => <Profile />) .default(() => <Home />), )
You can call match
with any primitive value, computed function, or existing atom. The returned atom depends on the initial expression and contains the undefined
state by default. To add handlers and complete the state type, use chain methods. Each chain mutates the original atoms. It is a good practice to use it in the same place where atom was created.
default
for replacingundefined
fallback stateis
for strict comparisontruthy
(MDN) andfalsy
(MDN) for empty things handlingwith
structural handling
bind
Bind action or atom update function with passed callback.
import { action, createCtx } from '@reatom/core'import { bind } from '@reatom/lens'
const doSome = action()const ctx = createCtx()
export handleSome = bind(ctx, doSome)
handleSome(123)// 123
withReset
Adds reset
action to reset the atom state.
For example, clear state after all dependencies and subscribers are gone.
import { atom } from '@reatom/core'import { withReset } from '@reatom/lens'import { onDisconnect } from '@reatom/hooks'
export const dataAtom = atom([], 'dataAtom').pipe(withReset())onDisconnect(dataAtom, dataAtom.reset)
select
Sometimes you need to get computed value from an atom in another computer, but you don’t want to trigger other recomputations if the computed value is not changed. It is the common case for reatomComponent
from reatom/npm-react package. The select
allows you to perform and memorize some computation by a simple inline callback. It is specially useful when you can’t create separate memorized function because your target atom is dynamically created.
import { select } from '@reatom/framework'import { reatomComponent } from '@reatom/npm-react'
export const ListSize = reatomComponent(({ ctx }) => { // wrong way, the component will rerender on each element update const length = ctx.spy(listAtom).length // correct optimal way, the component will rerender only on `length` change const length = select(ctx, (ctx) => ctx.spy(listAtom).length)
return <div>{length}</div>}, 'ListSize')
Under the hood select
creates additional atom, so you can perform all regular tasks in the callback of the select
, just like in the regular computed atom.
Important note is that you could use only one select in each atom, this is done for performance reasons and API simplicity. If, for some reason, you really need a few select, you could nest it and call one in another and so on.