The rest of the implementation uses all the same concepts we learned previously.
When the searchingActor
completes successfully, we extract the posts from onDone
by accessing event.output
and transition to a new "Idle" state:
The result returned from a
fromPromise
actor can be accessed insideonDone
asevent.output
.The type of
output
is inferred from the actor return type (Post[]
).
const machine = setup({
types: {
events: {} as Event,
context: {} as Context,
},
actors: { searchingActor },
}).createMachine({
context: initialContext,
initial: "Searching",
states: {
Searching: {
invoke: {
src: "searchingActor",
input: ({ context }) => ({ query: context.query }),
onDone: {
target: "Idle",
actions: assign(({ event }) => ({
// 👇 `event.output` contains result of `searchingActor`
posts: event.output,
})),
},
},
},
Idle: {},
},
});
Remember that actions can be executed with every state transition. In this case we add the
onUpdatePosts
action when transitioning from "Searching" to "Idle" insideonDone
.
Inside "Idle" we add a on
event handler to update the query and submit the search.
After the submit
event we transition to "Searching". This will trigger again invoke
, which will perform the request with the new query:
const machine = setup({
types: {
events: {} as Event,
context: {} as Context,
},
actors: { searchingActor },
}).createMachine({
context: initialContext,
initial: "Searching",
states: {
Searching: {
invoke: {
src: "searchingActor",
input: ({ context }) => ({ query: context.query }),
onDone: {
target: "Idle",
actions: assign(({ event }) => ({
posts: event.output,
})),
},
},
},
Idle: {
on: {
"update-query": {
actions: assign(({ event }) => ({
query: event.value,
})),
},
"submit-search": {
target: "Searching",
},
},
},
},
});
The component looks the same as useState
and useReducer
, but without useEffect
or any other logic inside it:
export default function Machine() {
const [snapshot, send] = useMachine(machine);
return (
<div>
<div>
<input
type="search"
value={snapshot.context.query}
onChange={(e) =>
send({ type: "update-query", value: e.target.value })
}
/>
<button type="button" onClick={() => send({ type: "submit-search" })}>
Search
</button>
</div>
{snapshot.context.posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
))}
</div>
);
}
XState extracts side effect management from the component. All the logic is contained inside actors.
This example shows how XState can implement many patterns by combining actors and state machines.
Notice how we did not introduce any new concept, we simply used the same features we learned in the previous modules (actors, actions, states).
Nonetheless, we were able to implement a new pattern by understanding how machines are executed.
Every time we enter "Searching" a new request is performed using the current query from context. This pattern make state management more declarative, since we don't need to manually trigger requests every time.
It also allows avoiding useEffect
completely, making the logic more predictable.