/* eslint jsx-a11y/no-autofocus: off */ import { Provider, useAtom } from 'jotai/react'; import { atom } from 'jotai/vanilla'; import { atomWithActor, atomWithActorSnapshot, atomWithMachine, RESTART, } from 'jotai-xstate'; import { useEffect } from 'react'; import { assign, fromPromise, setup } from 'xstate'; const createEditableMachine = (value: string) => setup({ types: { events: {} as | { type: 'dblclick' } | { type: 'cancel' } | { type: 'commit'; value: string }, context: {} as { value: string }, }, actions: { commitValue: assign({ value: ({ event }) => { if (event.type !== 'commit') throw new Error('Invalid transition'); return event.value; }, }), }, }).createMachine({ id: 'editable', initial: 'reading', context: { value, }, states: { reading: { on: { dblclick: 'editing', }, }, editing: { on: { cancel: 'reading', commit: { target: 'reading', actions: { type: 'commitValue' }, }, }, }, }, }); const defaultTextAtom = atom('edit me'); const editableMachineAtom = atomWithMachine((get) => createEditableMachine(get(defaultTextAtom)), ); const Divider = () => { return (
); }; const Toggle = () => { const [state, send] = useAtom(editableMachineAtom); return (

Machine Atom

{state.matches('reading') && ( send({ type: 'dblclick' })}> {state.context.value} )} {state.matches('editing') && ( send({ type: 'commit', value: e.currentTarget.value })} onKeyDown={(e) => { if (e.key === 'Enter') { send({ type: 'commit', value: e.currentTarget.value }); } if (e.key === 'Escape') { send({ type: 'cancel' }); } }} /> )}

Double-click to edit. Blur the input or press enter to commit. Press esc to cancel.
); }; type PromiseLogicOutput = string; type PromiseLogicInput = { duration: number }; type PromiseLogicEvents = | { type: 'elapsed'; value: number } | { type: 'completed' }; const promiseLogicAtom = atom( fromPromise( async ({ emit, input }) => { const start = Date.now(); let now = Date.now(); do { await new Promise((res) => setTimeout(res, 200)); emit({ type: 'elapsed', value: now - start }); now = Date.now(); } while (now - start < input.duration); emit({ type: 'completed' }); return 'Promise finished'; }, ), ); const durationAtom = atom(5000); const promiseActorAtom = atomWithActor( (get) => get(promiseLogicAtom), (get) => { const duration = get(durationAtom); return { input: { duration } }; }, ); const promiseSnapshotAtom = atomWithActorSnapshot((get) => get(promiseActorAtom), ); const elapsedAtom = atom(0); const PromiseActor = () => { const [actor, send] = useAtom(promiseActorAtom); const [snapshot, clear] = useAtom(promiseSnapshotAtom); const [elapsed, setElapsed] = useAtom(elapsedAtom); const [input, setInput] = useAtom(durationAtom); useEffect(() => { const elapsedSub = actor.on('elapsed', (event) => setElapsed(event.value)); const completedSub = actor.on('completed', () => window.alert('Promise completed'), ); return () => { elapsedSub.unsubscribe(); completedSub.unsubscribe(); }; }, [actor, setElapsed]); return (

Promise actor atom

{snapshot.status === 'active' && `Waiting on promise. Elapsed ${Math.floor(elapsed / 1000)} out of ${Math.floor(input / 1000)} seconds`} {snapshot.status === 'done' && snapshot.output}
); }; const App = () => ( ); export default App;