Unleashing the Power of Actors in Frontend Application Development
Tags: javascript, react, webdev, frontend
Published: 2024-03-22 09:45:47

In backend development, threads have traditionally been used to achieve concurrency, which aims to maximize computing power. However, issues arise from the way threads communicate via shared mutable state or memory. Problems like race conditions, memory visibility, and deadlock occur when synchronizing this shared state. Addressing these problems adds complexity and makes development challenging.

On the other hand, actors eschew shared memory entirely. Each actor in the system possesses mutable state but refrains from sharing it with other actors:

> Actor programming retains mutable state but avoid sharing it. 
_Source: Seven Concurrency Models in Seven Weeks_

Also, actors run concurrently with each other and communicate via messaging.

This innovative model is employed by languages, including Elixir to achieve fault tolerance and resilience in software.

In this article, we will explore how to harness the actor paradigm in frontend development to construct robust, scalable, and maintainable software.

## Why Actors in the Frontend?
State management can be challenging, particularly in frontend applications. When developing a non-trivial application using React, the typical approach involves employing a state management library like Redux, Alt, or similar tools. These tools advocate for a "single source of truth," where all application state resides in a single object accessible to all components. This simplifies tasks such as hydration and debugging:

> [...] This makes it easy to create universal apps, as the state from your server can be serialized and hydrated into the client with no extra coding effort. A single state tree also makes it easier to debug or inspect an application. 
_Source: [Redux Style Guide](https://redux.js.org/understanding/thinking-in-redux/three-principles)_

However, this approach often leads to unnecessary re-renders. Whenever the state changes, all components reliant on data from the central state re-render, resulting in performance issues. To mitigate this, techniques like memoization, caching, and selectors are commonly employed. While effective, these solutions introduce additional complexity to the codebase.

Actors offer an alternative by avoiding the concept of a "single source of truth." Instead, each actor maintains its own private local state: 

> An actor is an independent virtual processor with its own local (and private) state.
_Source: The Pragmatic Programmer_

This characteristic of the actor model can be leveraged in frontend applications.

Before delving into the implementation of actors in the frontend, let's explore the three fundamental characteristics of actors.

### Send
In the actor model, communication between actors occurs through the exchange of messages. These messages are immutable and are sent asynchronously in a fire-and-forget manner. Additionally, actors handle messages they receive one at a time.

In essence, the actor model operates entirely through messaging:

> […] the most important aspect of actors is that you build applications by sending and receiving messages. 
_Source: Akka in Action_
 
### Create 
Actors have the ability to create or spawn additional actors, thus establishing a system. This system resembles a tree structure, allowing any actor, irrespective of its location, to communicate with all other actors within the system.

### Designing the Next Behavior
When an actor receives a message, it may change its behavior, which will be used for the next message it receives. For instance, imagine an actor that fetches data from an external API. It can exist in *idle*, *loading*, *success*, or *error* states.

So, if the actor is in the *idle* state and receives a message to load data, it will change its state to *loading*. When the next message arrives, the current state will be *loading*. Essentially, that's how actors change their behavior or state.

Finite state machines are useful for modeling actor behaviors.

## Implementing Actors in Frontend Applications
[XState](https://stately.ai/docs) is an excellent library that simplifies the utilization of actors in JavaScript applications. While this article focuses on using React, these principles apply equally well to other frameworks. In fact, they can be implemented anywhere JavaScript is executed.

### Creating Actors
For actors to communicate with each other, they need to be part of the same system. The most effective approach is to establish one parent actor, which serves as the root actor. From this root actor, we can create a hierarchy of actors that can exchange messages with one another.

In this example, we'll utilize React's context API. However, there’s another way, which is highlighted in this [article](https://stately.ai/blog/2024-02-12-xstate-react-global-state) on the Stately blog.

```javascript
const rootMachine = createMachine({
  entry: [
    spawnChild(cartActor, { systemId: "cart" }),
    spawnChild(favoritesActor, { systemId: "favorites" }),
  ],
});

export const rootContext = createActorContext(rootMachine);

const RootContext = ({ children }) => {
  return <rootContext.Provider>{children}</rootContext.Provider>;
};

export default RootContext;
```
The `createActorContext(rootMachine)` function creates an actor, specifically the root actor, and makes it accessible throughout our application via React Context. More information about this function can be found in the [XState docs](https://stately.ai/docs/xstate-react#createactorcontextlogic).

The `spawnChild()` function is employed to create child actors. It requires the logic of our actor as the first argument and an object with a `systemId` as the second argument.

Afterward, we can use the assigned `systemId`, represented as a string, to send messages to the actor or to access its internal state from a component using `useSelector()` or from another actor using `getSnapshot()`.

### Actor Communication 
As mentioned earlier, actors can exchange messages with other actors. In XState, this process is straightforward. To maintain consistency, every component containing logic should be linked to an actor. Moreover, components should exclusively communicate with their associated actors.

Using the `actorRef`, we can dispatch messages to the actor either from within our component or from another actor.

Suppose we aim to add a product to our cart whenever the `addToCart` button is clicked. This operation involves four steps:

#### First Step
When the user clicks on the button, we capture the click event  and send a message to the btnActor  using btnActorRef.send():

```javascript
const AddToCartBtn = ({ product: { btnActorRef, ...product } }) => {

  const addToCart = (e) => {
    e.preventDefault();
    btnActorRef.send({ type: "ADD_PRODUCT_TO_CART", product });
  };
  return (
    <button
      className="btn bg-[#EF6024] border-[#EF6024] hover:scale-105 hover:bg-[#EF6024] hover:border-[#EF6024] w-full text-white"
      onClick={addToCart}
    >
      Add To Cart
    </button>
  );
};
```

#### Second Step
The `btnActor` would receive the message, possibly execute some actions,  and subsequently send another message to the `cartActor` using the `sendTo()` function.

The first argument the `sendTo()` receives is the `actorRef`, which we can obtain using `system.get()` along with the `systemId` we previously assigned. In this case, it would be `cart`:

```javascript
import { createMachine sendTo, assign } from "xstate";

export const addToCartBtnActor = createMachine({
      on: {
        ADD_PRODUCT_COUNT: {
          target: "active",
          actions: sendTo(
	    ({ system }) => system.get("cart"),
            ({ event }) => ({ type: "ADD_TO_CART", product: 
            event.product })),
		},
	}
});
```

#### Third Step
The `cartActor` receives the message and updates its internal cart store accordingly, either by adding the product or by updating its count if the product already exists:

```javascript
import { createMachine, assign } from "xstate";

export const cartActor = createMachine({
  context: {
    cart: [],
		cartCount: 0,
  },
  on: {
    ADD_TO_CART: {
      actions: [assign({
      cart: ({ context, event }) => {
        const isInCart = context.cart.find(
          (product) => product.id === event.product.id
        );
        if (isInCart) {
          return context.cart.map((product) => {
            if (product.id === isInCart.id)
              return { ...product, count: product.count + 1 };
            return product;
          });
        }
        return context.cart.concat({ ...event.product, count: 1});
      },
    }),
  assign({
     cartCount: ({ context }) => context.cart.map((item) => item.count).reduce((acc, cv) => acc + cv, 0),
    })],
 },
});
```

#### Fourth Step
Finally, the `cartActor` updates its state accordingly, which will reflect in the cart component:

```javascript
import { useSelector } from "@xstate/react";
import { rootContext } from "../../RootContext";
import { HiShoppingCart } from "react-icons/hi2";

const Cart = () => {
  const machineRef = rootContext.useActorRef().system.get("cart");
  const { cartCount } = useSelector(machineRef, (state) => state.context);
  return (
    <div className="indicator grid grid-cols-2">
      <span className="indicator-item badge badge-secondary bg-red-400">
        {cartCount}
      </span>
      <HiShoppingCart className="text-2xl" />
    </div>
  );
};
```

### Designing the Next Behavior
Whenever an actor receives a message, it can decide to change its state and perform certain side effects or actions accordingly: 

> When an event instance is dispatched, the state machine responds by performing actions, such as changing a variable, performing I/O, invoking a function, generating another event instance, or changing to another state. _Source: [A crash course in UML state machines
](https://www.embedded.com/a-crash-course-in-uml-state-machines-part-1/)_

For instance, when the user clicks on the like button of a product, the actor representing the favorites component might toggle the product's like state. If the product was initially liked, it would be unliked, and vice versa. Below is the logic for the favorites component actor:

```javascript
import { createMachine } from "xstate";
const favoritesActor = createMachine({
  id: 'favorites',
  initial: 'unliked',
  states: {
    liked: {
      on: {
        TOGGLE: 'unliked'
      }
    },
    unliked: {
      on: {
        TOGGLE: 'liked'
      }
    }
  }
});
```
This machine has two states, `liked` and `unliked`, and transitions between them occur when the `TOGGLE` event is triggered.

### Conclusion
The actor paradigm presents a promising approach to developing robust and scalable frontend applications. By steering clear of a global shared state and promoting independent communication through messaging, actors can alleviate the challenges linked with state management. As the industry persists in adopting new paradigms, the actor model retains its value as a potent tool for attaining high-performance and dependable frontend solutions.