Hone logo
Hone
Problems

React Machine: Implementing a Finite State Machine Hook

This challenge asks you to create a custom React hook, useMachine, that manages the state of a finite state machine (FSM). FSMs are powerful for modeling complex application states and transitions, making your UI more predictable and easier to manage.

Problem Description

You need to implement a useMachine hook in TypeScript that allows developers to define and manage a finite state machine within their React components. The hook should accept an FSM definition and return the current state and a send function to dispatch events.

Key Requirements:

  1. FSM Definition: The hook should accept an FSM definition object. This object should contain:
    • initialState: The initial state of the machine.
    • states: An object where keys are state names and values are objects representing the transitions for that state. Each state object should contain an on property.
    • states[stateName].on: An object where keys are event names and values are the target states or actions to execute.
      • If the value is a string, it represents the target state to transition to.
      • If the value is a function, it represents an action to execute when the event occurs. The function can optionally return a target state.
  2. State Management: The hook must maintain the current state of the FSM.
  3. Event Dispatching: The hook must return a send function. Calling send(eventName) should trigger the corresponding transition or action based on the current state and the FSM definition.
  4. Type Safety: The hook and its usage should be strongly typed using TypeScript. This includes defining types for states, events, and actions.
  5. Context (Optional but Recommended for Advanced): Consider how you might pass context data to action functions. For this challenge, let's keep actions simple and not require context for the basic implementation.

Expected Behavior:

  • When the hook is initialized, it should set the FSM to its initialState.
  • When send(eventName) is called:
    • It should look up the current state in the states definition.
    • It should check if an on[eventName] transition or action is defined for the current state.
    • If a target state is specified, the hook should update the current state.
    • If an action function is specified, it should be executed. If the action function returns a state name, the machine should transition to that state.
    • If no transition or action is defined for the event in the current state, nothing should happen.
  • The hook should return the current state and the send function.

Edge Cases to Consider:

  • What happens if an event is sent for which there is no defined transition in the current state? (It should be ignored).
  • Ensure correct typing for states and events, especially when they are dynamic.

Examples

Let's define a simple traffic light FSM.

Example 1: Basic Transition

// FSM Definition
const trafficLightMachine = {
  initialState: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow',
      },
    },
    yellow: {
      on: {
        TIMER: 'red',
      },
    },
    red: {
      on: {
        TIMER: 'green',
      },
    },
  },
};

// Component Usage
function TrafficLight() {
  const [currentState, send] = useMachine(trafficLightMachine);

  return (
    <div>
      Current State: {currentState}
      <button onClick={() => send('TIMER')}>
        Advance
      </button>
    </div>
  );
}

// Initial Render:
// Current State: green

// After clicking "Advance" once:
// Current State: yellow

// After clicking "Advance" again:
// Current State: red

Example 2: State with Action

// FSM Definition
const notificationMachine = {
  initialState: 'idle',
  states: {
    idle: {
      on: {
        SHOW_NOTIFICATION: 'showing',
      },
    },
    showing: {
      on: {
        HIDE_NOTIFICATION: 'idle',
        'SHOW_NOTIFICATION': (state: string) => { // Action that can also transition
          console.log('Notification already showing. Ignoring.');
          return state; // Stay in the current state
        },
      },
    },
  },
};

// Component Usage
function Notification() {
  const [currentState, send] = useMachine(notificationMachine);

  return (
    <div>
      Current State: {currentState}
      <button onClick={() => send('SHOW_NOTIFICATION')}>
        Show Notification
      </button>
      {currentState === 'showing' && (
        <button onClick={() => send('HIDE_NOTIFICATION')}>
          Hide
        </button>
      )}
    </div>
  );
}

// Initial Render:
// Current State: idle

// After clicking "Show Notification":
// Current State: showing
// (Console logs: "Notification already showing. Ignoring.")

// After clicking "Hide":
// Current State: idle

Example 3: Invalid Event

// FSM Definition (same as Example 1)
const trafficLightMachine = {
  initialState: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow',
      },
    },
    yellow: {
      on: {
        TIMER: 'red',
      },
    },
    red: {
      on: {
        TIMER: 'green',
      },
    },
  },
};

// Component Usage
function TrafficLight() {
  const [currentState, send] = useMachine(trafficLightMachine);

  return (
    <div>
      Current State: {currentState}
      <button onClick={() => send('NON_EXISTENT_EVENT')}>
        Send Invalid Event
      </button>
    </div>
  );
}

// Initial Render:
// Current State: green

// After clicking "Send Invalid Event":
// Current State: green (no change, event is ignored)

Constraints

  • The FSM definition must be provided at hook initialization and should not change during the component's lifecycle for this challenge.
  • The state names and event names will be strings.
  • Action functions should be pure functions that do not rely on external mutable state (unless explicitly passed in as context, which is not a requirement for this challenge).
  • The hook should be performant and avoid unnecessary re-renders.

Notes

  • Consider using useReducer internally within useMachine as it's a natural fit for managing state transitions.
  • Think carefully about how to define the types for your FSM definition to ensure type safety. You'll likely need generic types.
  • The send function should be stable across renders (e.g., using useCallback).
  • How would you handle an action that returns void vs. an action that returns a string (new state)? The hook needs to accommodate both.
Loading editor...
typescript