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.
- The current state (
Key Requirements:
- State Management: The hook must maintain and update the current state correctly.
- Transition Logic:
- The
transitionsobject 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 } }
- The
- Event Handling: The returned transition function, when called with an event, should look up the appropriate transition in the
transitionsmap based on the current state and the provided event. - Valid Transitions: If a transition for the current state and the given event exists, the state should be updated to the next state.
- Invalid Transitions: If no valid transition is found for the current state and event, the state should remain unchanged.
- 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
transitionsmap is empty? - What happens if a state in the
transitionsmap 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
initialStatemust be of typeS. - The
transitionsobject must conform to theTransitionMap<S, E>type. - The hook should be implemented using React's
useStateanduseCallbackhooks. - The implementation should be performant for typical UI state management.
Notes
- Consider the type definition for
TransitionMap. A good starting point would beRecord<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
useCallbackfor 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
TransitionMaptype yourself.