UNPKG

react-machine

Version:

A lightweight state machine for React applications

142 lines (118 loc) 3.47 kB
import { useReducer, useEffect, useCallback, useMemo, useRef } from 'react' import { createMachine, transition, applyEffects, cleanEffects } from './core.js' function defaultAreEqual(prev, next) { if (prev === next) { return true } for (const i in prev) { if (prev[i] !== next[i]) return false } for (const i in next) { if (!(i in prev)) return false } return true } function initial({ machine, context }) { const initialState = { name: null } const initialEvent = { type: null } let [state, effects] = transition(machine, context, initialState, initialEvent) // minor optimisation to avoid a re-render if no effects have been queued if (!effects.some((eff) => eff.op === 'effect')) { effects = [] } const curr = { state, effects, context, } return curr } function reduce(curr, action) { if (action.type === 'send') { const { machine, context, runningEffects, event, options } = action const { assign, areEqual } = options const [state, effects] = transition(machine, context, curr.state, event, { assign }) if ( state === curr.state || (state.name === curr.state.name && state.data === curr.state.data && !effects.length) ) { if (areEqual(curr.context, context)) { return curr } else { return { ...curr, context } } } let nextEffects = [] if (curr.effects) { nextEffects = nextEffects.concat(curr.effects) } if (effects) { nextEffects = nextEffects.concat(effects) } // minor optimisation to avoid a re-render if no effects are running if (!runningEffects.current || !runningEffects.current.length) { if (!nextEffects.some((eff) => eff.op === 'effect')) { nextEffects = [] } } return { ...curr, state, effects: nextEffects, context } } if (action.type === 'flushEffects') { if (curr.effects.length) { return { ...curr, effects: [] } } else { return curr } } return curr } export function useMachine(create, context = {}, options = {}) { const { assign = 'assign', areEqual = defaultAreEqual } = options const contextRef = useRef(context) const runningEffects = useRef() const machine = useMemo(() => createMachine(create), [create]) const [curr, dispatch] = useReducer(reduce, { machine, context }, initial) const send = useCallback( (event) => dispatch({ type: 'send', context: contextRef.current, machine, runningEffects, event, options: { assign, areEqual }, }), [dispatch, machine, contextRef, assign, areEqual], ) // if context changed, we will transition // the machine if necessary if (assign && !areEqual(curr.context, context)) { dispatch({ type: 'send', machine, context, runningEffects, event: { type: assign }, options: { assign, areEqual }, }) } useEffect(() => { contextRef.current = context }) useEffect(() => { if (!curr.effects.length) return runningEffects.current = applyEffects( runningEffects.current, curr.effects, contextRef.current, send, ) dispatch({ type: 'flushEffects' }) }, [contextRef, dispatch, send, curr.effects]) useEffect(() => { return () => { runningEffects.current = cleanEffects(runningEffects.current) } }, []) return { state: curr.state, send, context, machine } }