koordinates / xstate-tree Goto Github PK
View Code? Open in Web Editor NEWBuild UIs with Actors using xstate and React
License: MIT License
Build UIs with Actors using xstate and React
License: MIT License
Not sure if this can be resolved with how our semantic-release workflow functions
History v4 is pretty old, should be updated to v5 when we can. This is blocked on legacy code in the Koordinates codebase that requires us to use history v4
Currently, the debug logging is totally unconfigurable. At the very least you should be able to turn it off somehow. But there are probably more things to improve
In Thomas Weber's master theisis that inspired xstate-tree, all events were global and sent to all actors. I felt that was overkill for xstate-tree so only specific opt-in events are sent globally, via the broadcast
method.
However now that we have been using that for a few years even just sending all events to all machines feels problematic, mostly because you need to start prefixing events since the names are global across all parts of the application. It feels like a better solution is to have mailboxes that have specific events attached to them that can be used in specific sections to better split up responsibilities. Then you can access this mailbox to send an event to all listeners of it, instead of funnelling all events through the same mailbox. It would also allow removing the somewhat strange GlobalEvent
interface merging system for typing global events.
Could make sense to have a routing mailbox too ๐ค
The xstate receptionist RFC seems relevant to this statelyai/rfcs#5
This prop is the only prop passed to views that is tricky to type correctly when using the view directly (ex in Storybook stories or tests) as it depends on typegen information from the machine. It's also better practice to use the same inState
function that is in the selectors to instead add something to the selectors to determine your conditional with in the view instead of using inState
directly in the view.
In light of that, inState
should be deprecated and when #14 is done the new system should not provide inState
to the view.
Not uncommon to have scenarios where you have a React view you want to be able to invoke into a slot in an xstate-tree machine, so you wrap it into a dummy machine that just renders the view.
Should have a helper utility that accepts the view and returns a machine so this requires no boilerplate
This is a not uncommon scenario, where you have a function that only exists in React land that you want to call from an event in an xstate machine. The current solution to that is to push the callback function into the machines context so it may call it.
With xstate 5.9.0s addition of event emitters it should be possible to come up with a solution that allows using event emitters to call supplied React functions passed as props to the slots view.
ie something like this
// In the child machine to be invoked into the slot
export type EmittedEvents = { type: "someEvent", bar: string}
const machine = setup({
types: {
emitted: {} as EmittedEvents
},
}).createMachine({
entry: emit({type: "some-event", bar: "foo})
})
// In some parent machine that uses the slot
const someSlot = singleSlot("SomeSlot").withEmittedEvents<EmittedEvents>();
const slots = [someSlot]
const machine = createMachine({
// .....
})
const XstateTreeMachine = createXStateTreeMachine(machine, {slots, View({slots}) {
return <slots.SomeSlot onSomeEvent={({bar}) => console.log(bar)} />
}})
The same sort of functionality should also be exposed on root components
The build* factory function pattern that is currently in use, primarily to aid in typing, is.... not the best. I have long wanted to replace it with a better system but I have not been able to come up with one. The important functionality of the current system is
Readme says slots is the defining feature of xstate-tree:
Actors views are composed together via "slots", which can be rendered in the view to provide child actors a place to render their views in the parent's view.
But it leaves it out of the example, so it's hard to see how slots would look and feel when using xstate-tree to its full extent:
// If this tree had more than a single machine the slots to render child machines into would be defined here
const slots = [];
Something like useIsRouteActive(route: AnyRoute): boolean
Should be fairly simple to implement by checking if the activeRouteEvents
array on the RoutingContext
contains the given route or not.
xstate supports tags on states. Currently you can't access that information in the selectors or views.
Passing the hasTags
function to the selector and view would be a simple solution. Tags are not typesafe currently, not sure if typegen will be updated to support them at some point, may make more sense to wait until then to not require a rework to add the type information. We don't use them internally at Koordinates.
Currently only CJS style code is shipped. Update package to ship ESM and CJS code and update package.json with ESM functionality
Support for xstate v5 will soon be released on the @next tag
Before making the repo public there are a number of things I want to do/must be done
Needs
Wants
There is an overview of the exported API in xstate-tree.api.md but that is rather lacking as far as actual API docs go. Need to investigate setting up an api-docs static site
Syncing external data into xstate-tree machines is something we do a lot of at Koordinates. Most commonly Observables representing selectors from redux or watched GraphQL queries, to bring global state into the machines. But we have some areas where we need to sync data from React land back into the machine at the top of the xstate-tree hierarchy. That has been implented fairly poorly with a combination of useMemo
and withContext
building a new root machine anytime the useMemo
dependencies change.
These are a very similar class of problems however, getting data from the outside world into the xstate machines context, with different implementations but the same mental model from inside the machine. Both ways can work with some sort of sync event sent to the machine whenever the external data changes allowing you to write a single sync event action to update context. They would need different events however as the view syncing and the observable syncing wouldn't know about each other.
Handwavey thought to add a new type declaration declaring viewInputs
or some such. That could then be tied into the event for syncing from the view, props on the component build by buildRootComponent
, and as an argument to the slot creator functions to require passing props to a slot in the view.
From there it's a useEffect
away from sending an event to the machine every time the props change to allow syncing the data into context.
import { UsesFoo } from "./SomeOtherChildMachine";
type ViewEvent<T> = { type: "xstate-tree.viewSync"; data: T };
type ViewProps = { foo: string };
type Events = ViewEvent<ViewProps>;
// specify props view generic of the slot factories
const slots = [singleSlot<ExtractViewProp<typeof UsesFoo>("UsesFoo")];
const machine = createMachine({
id: "example1",
schema: {
events: {} as Events,
viewProps: {} as ViewProps.
},
on: {
"xstate-tree.viewSync": {
actions: immerAssign((ctx, e) => {
ctx.foo = e.data.foo;
})
}
}
})
// snip
// foo prop in view passed in from above root component or machine
const view = buildView(..., ({ slots, foo }) => {
// And then this slot was typed as also taking a foo so it gets passed here
return <slots.UsesFoo foo={foo} />;
});
and with a root component
const Root = buildRootComponent(machine):
<Root foo="foo" />
This seems like it would require some sort of wrapper around a group of observables to emit a new value any time any of the observables changes, including the last emit from any other observables to ease writing the sync action handler (or would that be bad because you have no way of telling if an observable had emitted a new value ๐ค).
You would then need to invoke that wrapped observable in your machine and attach a sync event action to update context when it emits.
As much as I would like automatically inject the invocation of the observable to reduce boilerplate I don't think it's advisable as it won't show up in the xstate visualizer. The reason we have been rolling back any machine config generation helpers we have at Koordinates, breaks the visualizer as the source of truth.
// Basically just combineLatest from RxJS and mapping them to an event
const wrappedObservables$ = wrapThemUp({
foo: foo$,
bar: bar$
});
type ObservableSyncEvent<T> = { type: "xstate-tree.sync"; data: T };
type Events = ObservableSyncEvent<ATypeExtractor<typeof wrappedObservables$>>;
const machine = createMachine({
id: "example1",
schema: {
events: {} as Events,
},
invoke: {
src: wrappedObservables$
},
on: {
"xstate-tree.sync": {
actions: immerAssign((ctx, e) => {
ctx.foo = e.data.foo;
ctx.bar = e.data.bar;
})
}
}
}))
Currently the result of the buildActions
builder is cached until one of the following happens
And then the selectors can change for the following reasons
Which means that the actions will change reference whenever the machines context is updated or the state is changed. This is not great for re-renders and makes the DX of using an action inside of a useEffect
pretty awful. Can't refer to the action directly and need to use a ref that gets synchronized with the actions current reference.
It seems like a solution to this would be to instead return a stable Proxy reference for actions and have that proxy forward the call to the current actions reference.
There is a type, SharedMeta
, that is used by routing events to populate the meta object. Currently this contains a bit of Koordinates specific typing and has no way for consumers of the library to provide their own exta meta information.
A system to enable consumers to add their own shared meta so we can remove the Koordinates specific property on SharedMeta
solves both problems
The custom canHandleEvent
implementation predates state.can, but we should use that now that is available.
xstate-tree started as an internal package in Koordinates in 2020. Since it was originally designed for our internal use only there will inevitably be some issues that crop up or subpar decisions when operating it in another environment. Things like dependency versions, documentation, packaging style etc
This issue is for more "meta" bugs like that, bugs in the library code should go on their own issues.
Helper utility to remove boilerplate for machines of simple event -> state -> machine configurations as is common with routing machines. Something like
export const AppRouter = buildRoutingMachine([...routingEvents], {
GO_TO_FOO: FooMachine,
GO_TO_BAR: BarMachine
});
Which is analogous to
export const AppRouter = createMachine(
on: {
GO_TO_FOO: "foo",
GO_TO_BAR: "bar",
},
initial: "waitingForRoute",
states: {
waitingForRoute: {},
foo: {
invoke: { src: FooMachine }
},
bar: {
invoke: { src: BarMachine }
}
}
);
The test-app used by the current tests is... lacking. It was a simple attempt at todo-mvc from way back when this library was first created back in 2020 and has grown and changed randomly as the test suite needed it over the years. It does not provide enough testing flexibility either so it doesn't even fulfill it's only purpose very well
Need to create a new xstate-tree example app, with a good test suite, both to provide a good piece of documentation on what an xstate-tree app looks like and how to test one, as well as increase the testing confidence.
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.