useStateTransition.ts37 lines · main
| 1 | import { useEffect, useRef } from 'react' |
| 2 | |
| 3 | export function useStateTransition< |
| 4 | State extends { type: string }, |
| 5 | PrevType extends State['type'], |
| 6 | NewType extends State['type'], |
| 7 | >( |
| 8 | state: State, |
| 9 | // Documentary only — the entry-detection logic below intentionally does not |
| 10 | // require `savedPrevState.type === prevTest`. React 18+ auto-batches |
| 11 | // dispatches across awaits (e.g. dispatch SUBMIT → await → mutation |
| 12 | // onError dispatch ERROR), which collapses `prevTest → newTest` into a |
| 13 | // single render where the intermediate `prevTest` state is never observed, |
| 14 | // so the previous state may be any variant other than `newTest`. `cb`'s |
| 15 | // first parameter is typed accordingly. |
| 16 | _prevTest: PrevType, |
| 17 | newTest: NewType, |
| 18 | cb: ( |
| 19 | prevState: Exclude<State, { type: NewType }>, |
| 20 | currState: Extract<State, { type: NewType }> |
| 21 | ) => void |
| 22 | ): void { |
| 23 | const prevState = useRef(state) |
| 24 | |
| 25 | useEffect(() => { |
| 26 | const savedPrevState = prevState.current |
| 27 | |
| 28 | if (savedPrevState.type !== newTest && state.type === newTest) { |
| 29 | cb( |
| 30 | savedPrevState as Exclude<State, { type: NewType }>, |
| 31 | state as Extract<State, { type: NewType }> |
| 32 | ) |
| 33 | } |
| 34 | |
| 35 | prevState.current = state |
| 36 | }, [cb, newTest, state]) |
| 37 | } |