DEV Community

Mikey Stengel
Mikey Stengel

Posted on

State machine advent: Accessing private actor state in components (23/24)

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.

Discussion (0)