Hone logo
Hone
Problems

Implement a Custom useStateMachine Hook in React

This challenge asks you to build a custom React hook, useStateMachine, that mimics the behavior of a state machine. This hook will allow you to manage complex state transitions in a predictable and organized manner, which is particularly useful for handling scenarios like form wizard steps, animation states, or game logic.

Problem Description

You need to implement a custom React hook called useStateMachine that takes an initial state and a transition map, and returns the current state and a function to trigger transitions.

What needs to be achieved:

  • Create a hook useStateMachine<S, E>(initialState: S, transitions: TransitionMap<S, E>): [S, (event: E) => void]
    • S: The type of your states.
    • E: The type of your events.
    • initialState: The starting state of the machine.
    • transitions: A map defining how to transition from one state to another based on events.
  • The hook should return an array containing:
    • The current state (S).
    • A function ((event: E) => void) that accepts an event and attempts to transition the state machine.

Key Requirements:

  1. State Management: The hook must maintain and update the current state correctly.
  2. Transition Logic:
    • The transitions object will be a map where keys are states, and values are another map.
    • The inner map's keys are events, and its values are the next states.
    • Example structure: { StateA: { EventX: StateB, EventY: StateA }, StateB: { EventZ: StateC } }
  3. Event Handling: The returned transition function, when called with an event, should look up the appropriate transition in the transitions map based on the current state and the provided event.
  4. Valid Transitions: If a transition for the current state and the given event exists, the state should be updated to the next state.
  5. Invalid Transitions: If no valid transition is found for the current state and event, the state should remain unchanged.
  6. Reactivity: Changes to the state must trigger re-renders in components using the hook.

Expected Behavior:

  • On initial render, the hook should return the initialState.
  • When the transition function is called with a valid event, the current state should update to the corresponding next state.
  • When the transition function is called with an event that does not have a defined transition from the current state, the state should not change.

Important Edge Cases to Consider:

  • What happens if the transitions map is empty?
  • What happens if a state in the transitions map doesn't have any defined events?
  • Ensure type safety for states and events.

Examples

Let's define some types for our examples:

type LightSwitchState = 'off' | 'on';
type LightSwitchEvent = 'toggle';

type TrafficLightState = 'red' | 'yellow' | 'green';
type TrafficLightEvent = 'next';

type FormState = 'step1' | 'step2' | 'step3' | 'submitted';
type FormEvent = 'next' | 'back' | 'submit';

Example 1: Light Switch

// Initial State: 'off'
// Transitions:
//   'off' with 'toggle' -> 'on'
//   'on' with 'toggle' -> 'off'

const initialState: LightSwitchState = 'off';
const transitions: TransitionMap<LightSwitchState, LightSwitchEvent> = {
  off: { toggle: 'on' },
  on: { toggle: 'off' },
};

// Usage in a component:
// const [state, transition] = useStateMachine(initialState, transitions);
// state would initially be 'off'
// transition('toggle') would change state to 'on'
// transition('toggle') again would change state back to 'off'

Example 2: Traffic Light

// Initial State: 'green'
// Transitions:
//   'green' with 'next' -> 'yellow'
//   'yellow' with 'next' -> 'red'
//   'red' with 'next' -> 'green'

const initialState: TrafficLightState = 'green';
const transitions: TransitionMap<TrafficLightState, TrafficLightEvent> = {
  green: { next: 'yellow' },
  yellow: { next: 'red' },
  red: { next: 'green' },
};

// Usage in a component:
// const [state, transition] = useStateMachine(initialState, transitions);
// state would initially be 'green'
// transition('next') would change state to 'yellow'
// transition('next') again would change state to 'red'
// transition('next') again would change state to 'green'

Example 3: Form Navigation with Invalid Transition

// Initial State: 'step1'
// Transitions:
//   'step1' with 'next' -> 'step2'
//   'step2' with 'next' -> 'step3'
//   'step2' with 'back' -> 'step1'
//   'step3' with 'submit' -> 'submitted'

const initialState: FormState = 'step1';
const transitions: TransitionMap<FormState, FormEvent> = {
  step1: { next: 'step2' },
  step2: { next: 'step3', back: 'step1' },
  step3: { submit: 'submitted' },
  // 'submitted' state has no outgoing transitions defined
};

// Usage in a component:
// const [state, transition] = useStateMachine(initialState, transitions);
// state starts as 'step1'
// transition('next') -> 'step2'
// transition('back') -> 'step1'
// transition('submit') from 'step1' is invalid, state remains 'step1'
// From 'step3', transition('back') is not defined, state remains 'step3'.
// From 'submitted', any event like 'next' will not change the state.

Constraints

  • The initialState must be of type S.
  • The transitions object must conform to the TransitionMap<S, E> type.
  • The hook should be implemented using React's useState and useCallback hooks.
  • The implementation should be performant for typical UI state management.

Notes

  • Consider the type definition for TransitionMap. A good starting point would be Record<S, Record<E, S>>. However, you might want to make it more flexible to handle cases where an event might not lead to a state, or where not all states have outgoing transitions. Think about how to represent these optional transitions.
  • Using useCallback for the transition function is important to prevent unnecessary re-creations of the function on every render, which can optimize performance for child components that might receive this function as a prop.
  • You'll need to define the TransitionMap type yourself.
Loading editor...
typescript