carloslfu / xstate-router Goto Github PK
View Code? Open in Web Editor NEWXState Router. Add routes to your XState machine.
License: MIT License
XState Router. Add routes to your XState machine.
License: MIT License
It seems like the graph methods were moved to a separate repo @xstate/graph
so this package doesn't work with xstate@^4.7.0
.
I know it hasn't been updated in a while, but I haven't found another solution for a wizard form.
If one sends an event which targets a state having a parameterized route and the context reflects this parameters in the "match" property like it would be as typing a parameterized route into the browsers location bar then the route shown in the browsers location bar does no show the current parameters but the placeholders instead.
I adopted the sandbox example (https://codesandbox.io/s/xstate-router-5r5r7) like this:
Loading https://5r5r7.codesandbox.io/#/dashboard/4711/data gives
State: {"dashboard":{"loggedIn":"data"}}
as expected. If you load https://5r5r7.codesandbox.io/#/about and press the "Next" button you will get
https://5r5r7.codesandbox.io/#/dashboard/**:id**/data
as location instead of https://5r5r7.codesandbox.io/#/dashboard/**815**/data
I've added a sandbox-fork which show this: https://codesandbox.io/s/xstate-router-no0r3
Hi @carloslfu. Thank you very much for this useful library.
I tried to make it work in my react-app/typescript project but I am getting the following error when I build my project with your lib:
./node_modules/xstate-router/lib/index.js
Module parse failed: Unexpected token (68:21)
You may need an appropriate loader to handle this file type.
| exports.getRoutes = getRoutes;
| function addRouterEvents(history, configObj, routes) {
| const config = { ...configObj };
| if (!config.on) {
| config.on = {};
This is due to the "target" set in your tsconfig. I forked your project and changed
"target": "es2018",
to
"target": "es2016",
This causes the JS build to change from
const config = { ...configObj };
to
const config = Object.assign({}, configObj);
After making this change in my fork, my project builds with your lib without issues.
Hello,
I'm evaluating to use your library for state based routing. In our application there are states which must not be leaved immediately on changing the route. In some situations data needs to be safed using a REST-API before the state transition takes place (deferred state transition). In other situations in which the user fills a form I want to ask him for really changing the state (and may be loosing data) which might gives a deferred state transition (user chooses "yes") or no state transition (user chooses "no") and a "rollback" of the browsers history.
I'm not sure how to achieve this behavior. A first idea was to model events for route changes because for thinking in states and events this is also an event like hitting the submit button of a form.
Studding your code a little bit I could see that the library adds additional events to trigger transitions once the user enters a new route. My requirement shows that at least in some situations it is the responsibility of the application to model which events should be processed, isn't it? I understand that you use events because you store routing-data in the machine's context which is not allowed to be changed without an event. Additionally you want to provide this as an out-of-the-box functionality.
So my questions are:
If you think my suggestions are good then I could fork your project and try to do the modifications to send you a pull request. But I think I will need your help. What do you think?
Cheers,
Stephan
After merging #3 , there is a test failing, this needs investigation in order to be able to release a new version to NPM.
Test: XStateRouter โบ When enter a state having a parameterized route, the route sho
uld reflect the current param value. - line 180
expect(received).toBe(expected) // Object.is equality
Expected: "/substate/817/c"
Received: "/substate/undefined/c"
178 | const { getByTestId } = renderWithRouter(App, { route: '/about' })
179 | fireEvent.click(getByTestId('go-substate-c'))
> 180 | expect(getByTestId('location-display').textContent).toBe('/substate/817/c')
| ^
181 | })
I ran into a bug with your library which caused the state machine to "reactivate itself" even tho the parent component had unmounted before. It turned out that you're not unsubscribing from history changes here
Line 197 in 429678e
// unsubscribe should be returned
return history.listen ...
and
Line 167 in 429678e
// return unsubscribe in the useEffect hook for proper cleanup
return handleTransitionEvents
I'm noticing that route-changed
events are triggered even if the machine gets initialized on that particular route. This leads to duplicate actions on state entry and invocations of services at the target route.
I've isolated the behavior in this sandbox:
https://codesandbox.io/s/xstate-router-forked-fyff8
my suggestion is to omit the route-changed
event and not trigger it if the route did not change or the previous event was xstate.init
.
In case I want to mix states that should have routes together with states that shouldn't (due to their transitive nature) in the same machine, how do I express this? Just leaving out the meta: { path: '/whatever }
will drive the router into a runtime-error, setting path to "null" doesn't change the outcome. Thanks in advance.
I am using the useRouterMachine
where all states have a path in each meta object.
const history = createBrowserHistory();
const { state, send, service } = useRouterMachine({
config,
options,
initialContext,
history,
});
Each state has a NEXT and PREV event to navigate between states forwards and backwards. This works without a problem.
But the minute I click the browser back button, which triggers history action POP
, nothing happens on the page (meaning it doesn't jump back) and the browser console spits out this message:
Warning: Event "route-changed" was sent to stopped service "(machine)". This service has already reached its final state, and will not transition.
Event: {"type":"route-changed","dueToStateTransition":false,"route":"/enrolment/declaration","service":{"id":"(machine)"}}
Now since useRouterMachine
uses useMachine
from @xstate/react
I realised that it stops the service on unmount https://github.com/davidkpiano/xstate/blob/master/packages/xstate-react/src/index.ts#L97 .. and on history POP
the component gets unmounted, hence pushing event route-changed
results in this error.
The issue exacerbates the second I fire any other event like NEXT
on my state machine, which triggers an infinite loop (resulting in the same warning in the console showing like 10 times / second) which breaks the browser and I have to force close it.
I have a solution for this, will attach a PR shortly.
The routerMachine works with a class component like a charm. Except, in an all-functional components project like mine it fails to run and produces the following infiniate rerending error:
index.js:2178 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render....
Here's my code:
const Enrolment = () => {
const options = {
actions: {
...
},
};
const initialContext = {};
const { state, send } = routerMachine({
config: enrolmentMachineConfig,
options,
initialContext,
history,
});
const renderComponent = () => {
switch (state.value) {
case 'introduction':
return <Introduction />;
case 'emailCheck':
return <EmailCheck />;
...
default:
return null;
}
};
return (
<EnrolmentMachineContextProvider value={state.context}>
<EnrolmentMachineStateProvider value={state.value}>
<EnrolmentMachineSendProvider value={send}>
<>
<Header />
{renderComponent()}
<Footer />
</>
</EnrolmentMachineSendProvider>
</EnrolmentMachineStateProvider>
</EnrolmentMachineContextProvider>
);
};
export default Enrolment;
I tried using useService
from @xstate/react
but that still didn't help, and it seems that's because routerMachine
triggers service.onTransition
on every render.
The way I solved this problem is by forking your lib, refactoring routerMachine
so to reuse its functionality and created a new function: useRouterMachine
.
I refactored routerMachine
function with the to the following:
export function createRouterMachine<
TContext = any,
TState extends StateSchema = any,
TEvent extends EventObject = any
>({
config,
options = ({} as MachineOptions<TContext, TEvent>),
initialContext = {},
history = createBrowserHistory(),
}: RouterArgs): StateMachine<TContext, TState, TEvent> {
const routes = getRoutes(config)
const enhancedConfig = addRouterEvents(history, config, routes)
const currentLocation = history.location
const enhancedContext = {
...initialContext,
match: resolve(routes, currentLocation),
location: currentLocation,
history
}
return Machine(enhancedConfig, options, enhancedContext);
}
export function routerMachine<
TContext = any,
TState extends StateSchema = any,
TEvent extends EventObject = any
>({
config,
options = ({} as MachineOptions<TContext, TEvent>),
initialContext = {},
history = createBrowserHistory(),
}: RouterArgs) {
const machine = createRouterMachine({config, options, initialContext, history})
const service = interpret(machine)
service.start()
handleTransitionEvents(service, history, getRoutes(config))
return service
}
export function useRouterMachine
<
TContext = any,
TState extends StateSchema = any,
TEvent extends EventObject = any
>({
config,
options = ({} as MachineOptions<TContext, TEvent>),
initialContext = {},
history = createBrowserHistory(),
}: RouterArgs) {
const machine = createRouterMachine({config, options, initialContext, history})
const [state, send, service] = useMachine(machine);
useEffect(() => {
handleTransitionEvents(service, history, getRoutes(config))
}, [])
return {state, send, service};
}
export function handleTransitionEvents (service, history, routes) {
let debounceHistoryFlag = false
let debounceState = false
handleRouterTransition(history.location)
service.onTransition(state => {
const stateNode = getCurrentStateNode(service, state)
const path = findPathRecursive(stateNode)
if (debounceState
// debounce only if no target for event was given e.g. in case of
// fetching 'route-changed' events by the user
&& debounceState[1] === path) {
debounceState = false
return
}
if (!matchURI(path, history.location.pathname)) {
debounceHistoryFlag = true
const uri = buildURI(path, state.context.match)
history.push(uri)
service.send({ type: routerEvent, dueToStateTransition: true, route: path, service: service })
}
})
history.listen(location => {
if (debounceHistoryFlag) {
debounceHistoryFlag = false
return
}
handleRouterTransition(location, true)
})
function handleRouterTransition(location, debounceHistory?: boolean) {
let matchingRoute
for (const route of routes) {
const params = matchURI(route[1], location.pathname)
if (params) {
matchingRoute = route
break
}
}
if (matchingRoute) {
debounceState = matchingRoute[1] // debounce only for this route
service.send({ type: routerEvent, dueToStateTransition: false, route: matchingRoute[1], service: service })
const state = service.state.value
if (!matchesState(state, matchingRoute[0].join('.'))) {
const stateNode = getCurrentStateNode(service, service.state)
if (stateNode.meta && stateNode.meta.path) {
if (debounceHistory) {
debounceHistoryFlag = true
}
history.replace(stateNode.meta.path)
}
}
}
}
}
What do you think? Do you think it's worth including this change and renaming the project to xstate-react-router
?
By the way, I noticed you have 15 months old repo called xstate-react-router
which has a different type of implementation. I am not sure what you intend to do with that project although it seemed interesting enough to have a component wrapper around the react-router
Route
s.
xstate-router
is pointing to xstate-react-router
on github rather than xstate-router
, I think this is a mistake.Thanks a bunch!
Issa
I'm trying to do something like:
const Private = {
entry: send ('test'),
...
But even though the event is emitted it does not transition the machine to the 'test' state.
The purpose of this is to prevent the user from entering the Private area if there is no valid token.
The current setup does not allow passing interpreter options like {devTools: true}
.. I need it to be able to use the xstate devtools https://chrome.google.com/webstore/detail/xstate-devtools/aamnodipnlopbknpklfoabalmobheehc?hl=en
I will make a PR to enable both routerMachine
and useRouterMachine
to have interpreterOptions passed.
hi,
I found this project via your react router xstate integration repo, it seems to be abandoned. (last commit beginning of 2019)
This is the recommended repository for integrating a router in react using xstate ?
So I presume this replaces react router and just used pure xstate ?
Any help really appreciated
Thanks
Hi Carlos (@carloslfu),
Awesome library you have started here. Using the concepts, I re-implemented this library for personal use in JS/ES6 instead of TS. In the process, I may of simplified some of the state/route debouncing and initialization logic and also added a couple features I needed.
Please take a look and feel free to use the enhancements at: https://codesandbox.io/s/xstate-routed-machines-hpbl7
By adding a dynamically built transient initial state to the config, the routed machine now starts in the correct state when initializing at a path other than the 'initial' state's route. The metadata for a routed state node now uses a route object that allows for more control over matching and switching routes
meta: {
route: {
path: '/somePath/:someNamedParam/(orARegex)?',
exact: false, //allows for partial matching from start note the path '/' would match everything
priority: -1, //allows to prioritizing which route to match when several routes may match.
replace: false //if set to true will use history.replace instead of history.push to update the path
}
}
Thanks for your hard work,
Tim
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.