👻🤖
Jotai integration library for XState
https://jotai.org/docs/integrations/xstate
Creates an atom that creates, stores and manages an Actor, given it's logic. Follows mostly the same API as createActor
.
const promiseLogic = fromPromise(() => fetch('http://some.host/...')) // or fromTransition, fromObservable, fromEventObservable, fromCallback, createMachine
const actorAtom = atomWithActor(promiseLogic)
const Component = () => {
const [actorRef, send] = useAtom(actorAtom)
...
}
Can also be called with a second opts
argument for setting up actor parameters. In typescript it's important to correctly type the actors Input, Output and Events. Refer to the examples for a full implementation
const promiseLogic = fromPromise(({ input }: { input: { id: number } }) =>
fetch(`http://some.host/${input.id}`),
);
const actorAtom = atomWithActor(promiseLogic, { input: { id: 2 } });
// ^ Will type-error if you don't provide input
Either param can also be a Getter function, allowing you to derive data from other atoms
const promiseLogicAtom = atom(
fromPromise(({ input }: { input: { id: number } }) =>
fetch(`http://some.host/${input.id}`),
),
);
const idAtom = atom(2);
const actorAtom = atomWithActor(
(get) => get(promiseLogicAtom),
(get) => {
return { input: { id: get(idAtom) } };
},
);
You can fully type all inputs, outputs and events.
type PromiseLogicOutput = string;
type PromiseLogicInput = { duration: number };
type PromiseLogicEvents =
| { type: 'elapsed'; value: number }
| { type: 'completed' };
const promiseLogicAtom = atom(
fromPromise<PromiseLogicOutput, PromiseLogicInput, PromiseLogicEvents>(
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 actorAtom = atomWithActor((get) => get(promiseLogicAtom), {
input: { duration: 3000 },
});
const Component = () => {
// actorRef allows access to the return of 'createActor'
const [actorRef, send] = useAtom(actorAtom);
useEffect(() => {
const subscription = actorRef.on('elapsed', console.log);
return () => subscription.unsubscribe;
}, [actorRef]);
return ...
};
Important!!
By default atomWithActor
will call actor.start()
as soon as it mounts. To change this behaviour you can provide { autoStart: false }
in your options and start it manually
const promiseLogic = fromPromise(
() => new Promise((res) => setTimeout(res, 1000)),
); // or fromTransition, fromObservable, fromEventObservable, fromCallback, createMachine
const actorAtom = atomWithActor(promiseLogic, { autoStart: false });
const Component = () => {
const [actorRef, send] = useAtom(actorAtom);
return (
<button onClick={() => actorRef.start()}>
Click me to start the timeout
</button>
);
};
Provides access to an actors up-to-date snapshot
while also handling it's lifecycle and listeners. Takes in an instanced actor or a getter function that returns one.
type PromiseLogicOutput = string
// or fromTransition, fromObservable, fromEventObservable, fromCallback, createMachine
const promiseLogic = fromPromise<PromiseLogicOutput>(() => new Promise(
res => setTimeout(() => res("Return from inside logic"), 1000)
))
const actorAtom = atomWithActor(promiseLogic)
// Here get() is required because the actor logic is also stored in an atom
const actorSnapshot = atomWithActorSnapshot(get => get(actorAtom))
const Component = () => {
const [actorRef, send] = useAtom(actorAtom)
const [snapshot, clear] = useAtom(actorSnapshot)
return (
<div>
{snapshot.state === "active" && "Waiting on timeout"}
{snapshot.state === "done" && `Timeout done! Actor output is: ${snapshot.output}`}
</div>
)
...
}
Calling this atom's write
function (named clear
in the example above) will clear the internal snapshot and reset the listeners. This is usefull when combined with calling send(RESTART)
on the actor logic, especially when it depends on derived values.
type PromiseLogicOutput = string
type PromiseLogicInput = { duration: number }
// or fromTransition, fromObservable, fromEventObservable, fromCallback, createMachine
const promiseLogic = fromPromise<PromiseLogicOutput, PromiseLogicInput>(({input}) => new Promise(
res => setTimeout(() => res("Return from inside logic"), input.duration)
))
const durationAtom = atom(1000)
const actorAtom = atomWithActor(promiseLogic)
// Here get() is required because the actor logic is also stored in an atom
const actorSnapshot = atomWithActorSnapshot(get => get(actorAtom))
const Component = () => {
const [actorRef, send] = useAtom(actorAtom)
const [snapshot, clear] = useAtom(actorSnapshot)
const [duration, setDuration] = useAtom(durationAtom)
return (
<div>
{snapshot.state === "active" && "Waiting on timeout"}
{snapshot.state === "done" && (
<div>
Waited for {duration}ms
<br/>
Actor output is: {snapshot.output}
<button onClick={() => {
// The order here is important.
// First we set the duration
setDuration(duration + 1000)
// Then we reset the actor. This will start it with the new input
send(RESTART)
// Then we clear the snapshot, resetting the listeners on the new instance of the actor
clear()
}}>Wait {duration + 1000}ms</button>
</div>
)}
</div>
)
...
}