/* 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;