Compare commits

...

1 Commits

Author SHA1 Message Date
7ca51be5bc Hook react-router-dom into XState in one location 2025-04-08 13:23:53 -04:00
4 changed files with 146 additions and 3 deletions

View File

@ -18,7 +18,7 @@ import { markOnce } from '@src/lib/performance'
import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils' import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils'
import { trap } from '@src/lib/trap' import { trap } from '@src/lib/trap'
import type { IndexLoaderData } from '@src/lib/types' import type { IndexLoaderData } from '@src/lib/types'
import { settingsActor, useSettings } from '@src/machines/appMachine' import { appActor, settingsActor, useSettings } from '@src/machines/appMachine'
export const RouteProviderContext = createContext({}) export const RouteProviderContext = createContext({})
@ -34,6 +34,38 @@ export function RouteProvider({ children }: { children: ReactNode }) {
const location = useLocation() const location = useLocation()
const settings = useSettings() const settings = useSettings()
/**
* Spawn a router machine that we can interact with outside of React,
* but providing the react-router-dom methods and information we need.
*/
useEffect(() => {
appActor.send({
type: 'event:router_set_up',
data: {
location,
navigation,
navigate,
},
})
}, [])
/**
* "Subscribe" to the location and navigation from react-router-dom
* to keep the router actor up-to-date from here on out
*/
useEffect(() => {
appActor.getSnapshot().children.router?.send({
type: 'event:set_location',
data: location,
})
}, [location])
useEffect(() => {
appActor.getSnapshot().children.router?.send({
type: 'event:set_navigation',
data: navigation,
})
}, [navigation])
useEffect(() => { useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event. // On initialization, the react-router-dom does not send a 'loading' state event.
// it sends an idle event first. // it sends an idle event first.

View File

@ -1,5 +1,5 @@
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { createActor, setup, spawnChild } from 'xstate' import { createActor, InputFrom, setup, spawnChild } from 'xstate'
import { createSettings } from '@src/lib/settings/initialSettings' import { createSettings } from '@src/lib/settings/initialSettings'
import { authMachine } from '@src/machines/authMachine' import { authMachine } from '@src/machines/authMachine'
@ -9,13 +9,15 @@ import {
engineStreamMachine, engineStreamMachine,
} from '@src/machines/engineStreamMachine' } from '@src/machines/engineStreamMachine'
import { ACTOR_IDS } from '@src/machines/machineConstants' import { ACTOR_IDS } from '@src/machines/machineConstants'
import { routerMachine } from '@src/machines/routerMachine'
import { settingsMachine } from '@src/machines/settingsMachine' import { settingsMachine } from '@src/machines/settingsMachine'
const { AUTH, SETTINGS, ENGINE_STREAM } = ACTOR_IDS const { AUTH, SETTINGS, ENGINE_STREAM, ROUTER } = ACTOR_IDS
const appMachineActors = { const appMachineActors = {
[AUTH]: authMachine, [AUTH]: authMachine,
[SETTINGS]: settingsMachine, [SETTINGS]: settingsMachine,
[ENGINE_STREAM]: engineStreamMachine, [ENGINE_STREAM]: engineStreamMachine,
[ROUTER]: routerMachine,
} as const } as const
const appMachine = setup({ const appMachine = setup({
@ -23,6 +25,12 @@ const appMachine = setup({
children: { children: {
auth: typeof AUTH auth: typeof AUTH
settings: typeof SETTINGS settings: typeof SETTINGS
engine_stream: typeof ENGINE_STREAM
router?: typeof ROUTER
}
events: {
type: 'event:router_set_up'
data: InputFrom<typeof routerMachine>
} }
}, },
actors: appMachineActors, actors: appMachineActors,
@ -41,9 +49,30 @@ const appMachine = setup({
input: engineStreamContextCreate(), input: engineStreamContextCreate(),
}), }),
], ],
on: {
'event:router_set_up': {
actions: spawnChild(ROUTER, {
id: ROUTER,
systemId: ROUTER,
input: ({ event, context }) => {
if (event.type !== 'event:router_set_up')
return {
location: {} as InputFrom<typeof routerMachine>['location'],
navigation: {} as InputFrom<typeof routerMachine>['navigation'],
navigate: (() => {}) as InputFrom<
typeof routerMachine
>['navigate'],
}
return event.data
},
}),
},
},
}) })
export const appActor = createActor(appMachine) export const appActor = createActor(appMachine)
// REMOVE THIS BEFORE MERGING
window.appActor = appActor
/** /**
* GOTCHA: the type coercion of this actor works because it is spawned for * GOTCHA: the type coercion of this actor works because it is spawned for
* the lifetime of {appActor}, but would not work if it were invoked * the lifetime of {appActor}, but would not work if it were invoked

View File

@ -2,4 +2,5 @@ export const ACTOR_IDS = {
AUTH: 'auth', AUTH: 'auth',
SETTINGS: 'settings', SETTINGS: 'settings',
ENGINE_STREAM: 'engine_stream', ENGINE_STREAM: 'engine_stream',
ROUTER: 'router',
} as const } as const

View File

@ -0,0 +1,81 @@
import { useLocation, useNavigate, useNavigation } from 'react-router-dom'
import { assign, setup } from 'xstate'
export const routerMachine = setup({
types: {} as {
context: {
location: ReturnType<typeof useLocation>
navigation: ReturnType<typeof useNavigation>
navigate: ReturnType<typeof useNavigate>
}
input: {
location: ReturnType<typeof useLocation>
navigation: ReturnType<typeof useNavigation>
navigate: ReturnType<typeof useNavigate>
}
events:
| {
type: 'event:navigate'
data: Parameters<ReturnType<typeof useNavigate>>
}
| { type: 'event:set_location'; data: ReturnType<typeof useLocation> }
| { type: 'event:set_navigation'; data: ReturnType<typeof useNavigation> }
},
actions: {
navigate: (
_,
params: {
navigate: ReturnType<typeof useNavigate>
args: Parameters<ReturnType<typeof useNavigate>>
}
) => {
console.log("FRANK let's try and navigate", {
params,
})
params.navigate(...params.args)
},
},
}).createMachine({
id: 'router',
context: ({ input }) => ({
...input,
}),
on: {
'event:navigate': {
actions: {
type: 'navigate',
params: ({ event, context }) => ({
navigate: context.navigate,
args: event.data,
}),
},
},
'event:set_location': {
actions: assign({
location: ({ event }) => event.data,
}),
},
'event:set_navigation': {
actions: assign({
navigation: ({ event }) => event.data,
}),
},
},
// states: {
// idle: {
// on: {
// 'event:navigate:start': {
// target: 'navigating',
// actions: ['navigate'],
// },
// },
// },
// navigating: {
// on: {
// 'event:navigate:end': {
// target: 'idle',
// },
// },
// },
// },
})