Yesterday, we saw how to use the actor model to create hierarchy in applications and how to treat every actor as a computational unit that encapsulates behavior. Further, we established how actors have private state that can only be accessed from other actors using explicit communication (events). To build user interfaces, however, we do oftentimes want to access the private state of actors and render them to our UI. Today, we want to build a React component that renders the context
of the Player
actor.
Oftentimes, we can just mimic our actor architecture with React components. As a result, we can have a Game
component that invokes the gameMachine
and renders a Player
component to display the action (Rock, Paper, Scissors) the player performed. Meanwhile, the gameMachine
is a parent itself because it invokes the player
actor. Essentially, recreating the same hierarchy and relationships between components that we first defined within our machines.
We can iterate through the array that holds the references to the player actor and pass them to the child component as props which can then deal with them in two different ways as we'll see in a minute.
import { useMachine } from '@xstate/react';
import React, { Fragment } from 'react';
import { Player } from './Player';
const Game = () => {
const [state, send] = useMachine(gameMachine)
return (
<div>
{state.context.playerRefs.map((playerRef, index) => (
<Fragment key={index}>
<Player playerRef={playerRef} />
</Fragment>
))}
</div>
)
}
Once we define the Player
component, we have a decision to make. Do we only want to access the actor so that we can receive and send events to it or do we want to access its private state? Although not the goal for today, for the former option, we should go with the useActor
hook from the @xstate/react
package.
When using this hook, state
does not hold the context property since the actor state is private. Nonetheless, we could use the actor to send events from within our component.
import { useActor } from '@xstate/react';
import { PlayerActor } from './actorMachine'
const Player = ({playerRef}: {playerRef: PlayerActor }) => {
const [state, send] = useActor(playerRef);
// state.context === undefined
return null;
}
On the other hand, if we do want to access the context, we could make use of the running service which is another word for an invoked machine by using the useService
hook of the same package.
import { useService } from '@xstate/react';
import { PlayerService } from './actorMachine'
const Player = ({playerRef}: {playerRef: PlayerService }) => {
const [state, send] = useService(playerRef);
return (
<p>{state.context.identity} decided on: {state.context.playedAction}</p>
);
}
Passing the reference to the actor into the useService
subscribes the component to all the state changes of the actor. As a result, when the context or finite-state of the player actor changes, the component is rerendered as well. Needless to say, the reactive nature of state machines and React work harmoniously well together.
For a complete example, check the codesandbox for today's lecture and pay special attention to the type differences of the two respective hooks as pointed above (PlayerActor
vs PlayerService
).
About this series
Throughout the first 24 days of December, I'll publish a small blog post each day teaching you about the ins and outs of state machines and statecharts.
The first couple of days will be spent on the fundamentals before we'll progress to more advanced concepts.
Top comments (0)