Somebody definitely need to try give some Reatom examples to ChatGPT and ask to generate an explanation article.
Here is set of examples:
import { createCtx, action, atom } from '@reatom/core'
// create context in the app root
const ctx = createCtx()
// define your base mutable data references
// by passing a primitive initial values
const searchAtom = atom('')
const isSearchingAtom = atom(false)
const goodsAtom = atom<Array<Goods>>([])
// define computed atoms to infer data
// with smart and optimized caching
const tipAtom = atom((ctx) => {
// read and subscribe by `spy`
const goodsCount = ctx.spy(goodsAtom).length
if (goodsCount === 0) {
// read without subscribing by `get`
return ctx.get(searchAtom) ? 'Nothing found' : 'Try to search something'
}
if (goodsCount === 1) {
return `We found one treasure`
}
return `Found ${goodsCount} goods`
})
// define your actions to handle any IO and work with atoms
const onSearch = action((ctx, event) => {
// mutate base atoms by passing relative ctx and the new state
searchAtom(ctx, event.currentTarget.value)
})
const fetchGoods = action((ctx) => {
const search = ctx.get(searchAtom)
// [OPTIONAL] get your services from the context
const api = ctx.get(apiAtom)
// all sync updates inside action automatically batched
// and dependent computations will call after the action callback return
isSearchingAtom(ctx, true)
// schedule side-effects
// which will be called after successful execution of all computations
const promise = ctx.schedule(async () => {
const goods = await api.getGoods(search)
// pass a callback to `get` to batch a few updates inside async resolve
ctx.get(() => {
isSearchingAtom(ctx, false)
goodsAtom(ctx, goods)
})
})
// returned promise could be handled in place of the action call
return promise
})
import {
Action,
action,
Atom,
atom,
AtomMut,
throwReatomError,
} from '@reatom/core'
import { withReducers } from '@reatom/primitives'
import { noop, sleep } from '@reatom/utils'
import { getRootCause, onUpdate } from '@reatom/hooks'
export interface TimerAtom extends AtomMut<number> {
/** (delay - remains) / delay */
progressAtom: Atom<number>
/** interval in ms */
intervalAtom: AtomMut<number> & {
/** @deprecated extra thing */
setSeconds: Action<[seconds: number], number>
}
/** start timer by passed interval */
startTimer: Action<[delay: number], Promise<void>>
/** stop timer manually */
stopTimer: Action<[], void>
/** allow to pause timer */
pauseAtom: AtomMut<boolean>
/** switch pause state */
pause: Action<[], boolean>
/** track end of timer. Do not call manually! */
endTimer: Action<[], void>
}
export const reatomTimer = (
options:
| string
| {
name?: string
interval?: number
delayMultiplier?: number
progressPrecision?: number
resetProgress?: boolean
} = {},
): TimerAtom => {
const {
name = 'timerAtom',
interval = 1000,
delayMultiplier = 1000,
progressPrecision = 2,
resetProgress = true,
} = typeof options === 'string' ? { name: options } : options
const progressMultiplier = Math.pow(10, progressPrecision)
const timerAtom = atom(0, `${name}Atom`)
const progressAtom /* : TimerAtom['progressAtom'] */ = atom(
0,
`${name}.progressAtom`,
)
const pauseAtom: TimerAtom['pauseAtom'] = atom(false, `${name}.pauseAtom`)
const intervalAtom: TimerAtom['intervalAtom'] = atom(
interval,
`${name}.intervalAtom`,
).pipe(
withReducers({
setSeconds: (state, seconds: number) => seconds * 1000,
}),
)
const _versionAtom = atom(0, `${name}._versionAtom`)
const startTimer: TimerAtom['startTimer'] = action((ctx, delay: number) => {
delay *= delayMultiplier
throwReatomError(delay < ctx.get(intervalAtom), 'delay less than interval')
const version = _versionAtom(ctx, (s) => s + 1)
const start = Date.now()
let target = delay + start
let remains = delay
let pause = Promise.resolve()
let resolvePause = noop
timerAtom(ctx, remains)
progressAtom(ctx, 0)
pauseAtom(ctx, false)
const cleanupPause = onUpdate(pauseAtom, (pauseCtx, value) =>
getRootCause(ctx.cause) === getRootCause(pauseCtx.cause) &&
pauseCtx.schedule(() => {
if (value) {
const from = Date.now()
pause = new Promise((resolve) => {
resolvePause = () => {
target += Date.now() - from
resolve()
}
})
} else {
resolvePause()
}
}),
)
return ctx
.schedule(async () => {
while (remains > 0) {
await sleep(Math.min(remains, ctx.get(intervalAtom)))
await pause
if (version !== ctx.get(_versionAtom)) return
const batch = ctx.get.bind(ctx)
batch(() => {
remains = timerAtom(ctx, Math.max(0, target - Date.now()))
const interval = ctx.get(intervalAtom)
const steps = Math.ceil(delay / interval)
const stepsRemains = Math.ceil(remains / interval)
progressAtom(
ctx,
+(1 - stepsRemains / steps).toFixed(progressPrecision),
)
})
}
endTimer(ctx)
})
.finally(cleanupPause)
}, `${name}.startTimer`)
const stopTimer: TimerAtom['stopTimer'] = action((ctx) => {
_versionAtom(ctx, (s) => s + 1)
endTimer(ctx)
if (resetProgress) progressAtom(ctx, 0)
}, `${name}.stopTimer`)
const endTimer: TimerAtom['endTimer'] = action((ctx) => {
timerAtom(ctx, 0)
}, `${name}.endTimer`)
const pause: TimerAtom['pause'] = action(
(ctx) => pauseAtom(ctx, (s) => !s),
`${name}.pause`,
)
return Object.assign(timerAtom, {
progressAtom,
endTimer,
intervalAtom,
startTimer,
stopTimer,
pauseAtom,
pause,
})
}
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import { createTestCtx } from '@reatom/testing'
import { atom } from '@reatom/core'
import { onConnect } from '@reatom/hooks'
import { isDeepEqual, jsonClone, sleep } from '@reatom/utils'
import { reatomAsync, withDataAtom, withAbort } from '@reatom/async'
test('optimistic update without extra updates on invalidation', async () => {
//#region backend
let mock = [{ id: 1, value: 1 }]
const getData = async () => mock
const putData = async (id: number, value: number) => {
await sleep()
mock = jsonClone(mock)
mock.find((item) => item.id === id)!.value = value
}
//#endregion
// this is short for test purposes, use ~5000 in real code
const INTERVAL = 5
const fetchData = reatomAsync(getData, 'fetchData').pipe(
// add `dataAtom` and map the effect payload into it
// try to prevent new reference stream if nothing really changed
withDataAtom([], (ctx, payload, state) =>
isDeepEqual(payload, state) ? state : payload,
),
)
const updateData = reatomAsync(
(ctx, id: number, value: number) => putData(id, value),
{
name: 'updateData',
onEffect: (ctx, [id, value]) =>
fetchData.dataAtom(ctx, (state) =>
state.map((item) => (item.id === id ? { ...item, value } : item)),
),
},
)
onConnect(fetchData.dataAtom, async (ctx) => {
while (ctx.isConnected()) {
await fetchData(ctx)
await sleep(INTERVAL)
}
})
const ctx = createTestCtx()
const effectTrack = ctx.subscribeTrack(fetchData.onFulfill)
const dataTrack = ctx.subscribeTrack(fetchData.dataAtom)
// every subscription calls passed callback immediately
assert.is(effectTrack.calls.length, 1)
assert.is(dataTrack.calls.length, 1)
assert.equal(dataTrack.lastInput(), [])
// `onConnect` calls `fetchData`, wait it and check changes
await sleep()
assert.is(dataTrack.calls.length, 2)
assert.equal(dataTrack.lastInput(), [{ id: 1, value: 1 }])
// call `updateData` and check changes
updateData(ctx, 1, 2)
assert.is(dataTrack.calls.length, 3)
assert.equal(dataTrack.lastInput(), [{ id: 1, value: 2 }])
// wait for `fetchData` and check changes
assert.is(effectTrack.calls.length, 2)
await sleep(INTERVAL)
// the effect is called again, but dataAtom is not updated
assert.is(effectTrack.calls.length, 3)
assert.is(dataTrack.calls.length, 3)
// cleanup test
dataTrack.unsubscribe()
})
test('safe pooling', async () => {
const createTask = reatomAsync(async () => Math.random())
const tasks = new Map<number, number>()
const poolTask = reatomAsync(async (ctx, taskId: number) => {
ctx.controller.signal.aborted
await sleep(5)
const progress = (tasks.get(taskId) ?? -10) + 10
tasks.set(taskId, progress)
return progress
})
const progressAtom = atom(0)
const search = reatomAsync(async (ctx) => {
const taskId = await createTask(ctx)
while (true) {
const progress = await poolTask(ctx, taskId)
progressAtom(ctx, progress)
if (progress === 100) return
}
}).pipe(withAbort())
const ctx = createTestCtx()
const track = ctx.subscribeTrack(progressAtom)
const promise1 = search(ctx)
await sleep(15)
const promise2 = search(ctx)
await Promise.allSettled([promise1, promise2])
assert.is(ctx.get(progressAtom), 100)
assert.equal(
track.inputs(),
[0, 10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
)
})
test.run()
import { action, atom } from '@reatom/framework'
import { useAction, useAtom } from '@reatom/npm-react'
import './App.css'
// base mutable atom
const inputAtom = atom('', 'inputAtom')
// computed readonly atom
const greetingAtom = atom((ctx) => {
const input = ctx.spy(inputAtom)
return input ? `Hello, ${input}!` : ''
}, 'greetingAtom')
// a logic container
const onSubmit = action((ctx) => {
const greeting = ctx.get(greetingAtom)
// side-effects should be scheduled
// you could do it anywhere with `ctx`
ctx.schedule(() => alert(greeting))
}, 'onSubmit')
export default function App() {
const [input, setInput] = useAtom(inputAtom)
const [greeting] = useAtom(greetingAtom)
const handleSubmit = useAction(
(ctx, event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
onSubmit(ctx)
},
)
return (
<form onSubmit={handleSubmit}>
<h1>Reatom</h1>
<p>
<input
value={input}
onChange={(e) => setInput(e.currentTarget.value)}
placeholder="Your name"
/>
<button type="submit">Submit</button>
</p>
<p>{greeting}</p>
</form>
)
}
import { Action, action, atom, AtomMut, onUpdate, ParseAtoms, parseAtoms, withInit, random } from "@reatom/framework";
import { useAction, useAtom } from "@reatom/npm-react";
/*
You could store your data from the backend in atoms without any mappings, but it is a good practice to wrap some of your model slices to atoms for better control and to have access to more reactive features. The rule is simple: mutable properties should be an atom, readonly properties shout stay a primitive.
*/
type Field = {
id: number;
name: string;
value: AtomMut<string>;
remove: Action;
};
const KEY = "FIELDS";
const fromLS = () => {
const snap = localStorage.getItem(KEY);
if (!snap) return [];
const json: ParseAtoms<Array<Field>> = JSON.parse(snap);
return json.map(({ id, name, value }) => getField(id, name, value));
};
const toLS = action((ctx) => {
const list = parseAtoms(ctx, listAtom);
localStorage.setItem(KEY, JSON.stringify(list));
}, "toLS");
const getField = (id: number, name: string, value: string): Field => {
const field: Field = {
id,
name,
value: atom(value, `${name}FieldAtom`),
remove: action(
(ctx) => listAtom(ctx, (state) => state.filter((el) => el !== field)),
`${name}Field.remove`
),
};
onUpdate(field.value, toLS);
return field;
};
const listAtom = atom(new Array<Field>(), "listAtom").pipe(withInit(fromLS));
onUpdate(listAtom, toLS);
const newFieldAtom = atom("", "newFieldAtom");
const createField = action((ctx) => {
if (!ctx.get(newFieldAtom)) return
const field = getField(random(), ctx.get(newFieldAtom), "");
newFieldAtom(ctx, "");
listAtom(ctx, (state) => [...state, field]);
}, "createField");
const NewFieldComponent = () => {
const [input, setInput] = useAtom(newFieldAtom);
const handleCreate = useAction(
(ctx, event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
createField(ctx);
}
);
return (
<form onSubmit={handleCreate}>
<input placeholder="Name" value={input} onChange={(e) => setInput(e.currentTarget.value)} />
<button type="submit">Create</button>
</form>
);
};
const FieldComponent: React.FC<Field> = ({ name, value, remove }) => {
const [input, setInput] = useAtom(value);
const handleRemove = useAction(remove);
return (
<span>
<input value={input} onChange={(e) => setInput(e.currentTarget.value)} />
<button onClick={handleRemove}>del</button>
{` (${name}) `}
</span>
);
};
const ListComponent = () => {
const [list] = useAtom(listAtom);
return (
<ul>
{list.map((el) => (
<li key={el.id}>
<FieldComponent {...el} />
</li>
))}
</ul>
);
};
export default function App() {
return (
<main>
<NewFieldComponent />
<ListComponent />
</main>
);
}
import { useAction, useAtom } from "@reatom/npm-react";
import {
atom,
sample,
mapPayload,
onConnect,
onUpdate,
reatomAsync,
withDataAtom,
withReducers,
withAbort,
mapState
} from "@reatom/framework";
import { Lens } from "./Lens";
// `@reatom/async` docs
// https://reatom.dev/packages/async
type ImageData = { image_id: string; title: string };
export const fetchImages = reatomAsync(
(ctx, page: number = 1) =>
fetch(
`https://api.artic.edu/api/v1/artworks?fields=image_id,title&page=${page}&limit=${10}`,
ctx.controller
).then<{ data: Array<ImageData> }>((r) => r.json()),
"fetchImages"
).pipe(
withDataAtom([], (ctx, { data }) => data.filter((el) => el.image_id)),
withAbort({ strategy: "last-in-win" })
);
onConnect(fetchImages.dataAtom, fetchImages);
export const pageAtom = atom(1, "pageAtom").pipe(
withReducers({
next: (state) => state + 1,
prev: (state) => Math.max(1, state - 1)
})
);
onUpdate(pageAtom, fetchImages);
export const lastRequestTimeAtom = fetchImages.pipe(
mapPayload(0, () => Date.now(), "fetchStartAtom"),
sample(fetchImages.onSettle),
mapState((ctx, start) => start && Date.now() - start, "lastRequestTimeAtom")
);
const Paging = () => {
const [page] = useAtom(pageAtom);
const prev = useAction((ctx) => pageAtom.prev(ctx));
const next = useAction((ctx) => pageAtom.next(ctx));
return (
<>
<button onClick={prev}>prev</button>
<span> page: {page} </span>
<button onClick={next}>next</button>
</>
);
};
export default function App() {
const [lastRequestTime] = useAtom(lastRequestTimeAtom);
const [data] = useAtom(fetchImages.dataAtom);
const [isLoading] = useAtom((ctx) => ctx.spy(fetchImages.pendingAtom) > 0);
return (
<div>
<h1>artic.edu</h1>
<Paging />
<span>{!!isLoading && ` (Loading)`}</span>
<p>
<small>Loaded by {lastRequestTime}ms</small>
</p>
<ul>
{data.map(({ image_id, title }) => (
<Lens
key={image_id}
src={`https://www.artic.edu/iiif/2/${image_id}/full/843,/0/default.jpg`}
alt={title}
width={"20rem"}
height={"20rem"}
/>
))}
</ul>
</div>
);
}
import { atom, reatomAsync, withAbort, withDataAtom, withRetry, onUpdate, sleep } from "@reatom/framework";
import { useAtom } from '@reatom/npm-react'
import * as api from './api'
const searchAtom = atom('', 'searchAtom')
const fetchIssues = reatomAsync(async (ctx, query: string) => {
await sleep(350)
const { items } = await fetch(
`https://api.github.com/search/issues?q=${query}&page=${1}&per_page=10`, ctx.controller
).then<{ items: Array<{ title: string }> }>(async (r) => {
if (r.status !== 200) throw new Error(await r.text())
return r.json()
})
return items
}, 'fetchIssues')
.pipe(
withDataAtom([]),
withAbort({ strategy: 'last-in-win' }),
withRetry({
onReject(ctx, error: any, retries) {
return error?.message.includes('rate limit')
? 100 * Math.min(500, retries ** 2)
: -1
}
})
)
onUpdate(searchAtom, fetchIssues)
export default function App() {
const [search, setSearch] = useAtom(searchAtom)
const [issues] = useAtom(fetchIssues.dataAtom)
const [isLoading] = useAtom(ctx => {
// check the console and inspect nested `cause`s!
console.log(ctx)
return ctx.spy(fetchIssues.pendingAtom) + ctx.spy(fetchIssues.retriesAtom) > 0
})
return (
<main>
<input
value={search}
onChange={e => setSearch(e.currentTarget.value)}
placeholder="Search"
/>
{isLoading && 'Loading...'}
<ul>{issues.map(({ title }, i) => <li key={i}>{title}</li>)}</ul>
</main>
)
}
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too