artalar / reatom

Reatom - the ultimate state manager
https://reatom.dev
MIT License
989 stars 103 forks source link

Write a tutorial with ChatGPT #506

Open artalar opened 1 year ago

artalar commented 1 year ago

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>
  )
}
theghostbel commented 1 year ago

Explaining Reatom: Managing Application State in JavaScript

Overview

Reatom is a state management library for JavaScript that provides a simple and efficient way to manage state in applications. In this article, we'll take a closer look at how Reatom works by examining a code example that uses Reatom to manage state.

Understanding the Code

The code example uses Reatom to manage the state of an application. It starts by creating a context using the createCtx() function. The context is used to manage the state of the application.

The code defines three atoms to represent the state of the application: searchAtom, isSearchingAtom, and goodsAtom. These atoms are used to store primitive data types like strings and booleans, as well as arrays of objects.

Next, the code defines a tipAtom that infers data from the base atoms in a smart and optimized way. The tipAtom uses the spy and get methods to read and subscribe to the goodsAtom atom. Based on the length of the array returned by goodsAtom, the tipAtom returns a string that provides feedback to the user about the search results.

The code also defines two actions, onSearch and fetchGoods, which handle side effects and update the state in response to user events. The onSearch action updates the searchAtom atom with the user's search query. The fetchGoods action retrieves data from an API based on the search query stored in the searchAtom atom. The action updates the isSearchingAtom atom to indicate that the search is in progress, and schedules a side effect to retrieve the data. Once the data is retrieved, the action updates the goodsAtom atom with the results.

Conclusion

Reatom provides a simple and efficient way to manage state in JavaScript applications. By defining atoms to represent state and actions to handle side effects and state updates, developers can create applications that are easier to reason about and maintain.

artalar commented 1 year ago

We need a handbook with additional examples and so on! =D

benfrstr commented 7 months ago

Is this issue still open? I'd like to contribute.

Akiyamka commented 7 months ago

Is this issue still open? I'd like to contribute.

Sure, it would be great to add this to the recipes