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:
- 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 anonproperty.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.
- State Management: The hook must maintain the current state of the FSM.
- Event Dispatching: The hook must return a
sendfunction. Callingsend(eventName)should trigger the corresponding transition or action based on the current state and the FSM definition. - Type Safety: The hook and its usage should be strongly typed using TypeScript. This includes defining types for states, events, and actions.
- 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
statesdefinition. - 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.
- It should look up the current state in the
- The hook should return the current state and the
sendfunction.
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
useReducerinternally withinuseMachineas 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
sendfunction should be stable across renders (e.g., usinguseCallback). - How would you handle an action that returns
voidvs. an action that returns astring(new state)? The hook needs to accommodate both.