This is not a step-by-step guide or article on how you should but rather a how you could manage and share state when developing applications and components. I should also state, (see what I did there), that I am heavily influenced by Redux and less familiar with other state management libraries.
- Lift the machine just like you lift state.
- Import an interpreted machine and use useService
- Make a memoized version of useMachine
- Create a supervisor using Redux
- Use observable streams to share state
Current: You have component + machine that fetches and renders a list of todos.
Requirement: You need to display a count of completed todos in a sibling component.
Current: A component/machine combo in the top bar of your app fetches and displays the name of the authenticated user.
Requirement: Display the username in the footer as well.
Lift the machine interpreted using the useMachine hook up to a (grand)parent component if you find yourself needing to share it's state with a siblings. Use props or ReactJS' Context API to pass machine information (state) and callbacks (send) to (grand)child components.
Problem: Your "list of repositories" machine depends on a "selected user" machine higher up.
Requirement: When the selected user changes, the list of repositories should be reset and fetched again.
If your component + machine depends on a machine running higher up in the component hierarchy, (e.g. your machine could be a spawned actor). Don't use the useMachine hook in the child component but let the parent spawn it and pass data and callbacks down via props or use ReactJS' Context API.
The line of communcation here is:
spawnedMachine-> parentMachine -> ParentComponent -> ChildComponent -> ChildComponent. And back of course.
If you do not want your spawned repositories machine to make a network request when selected user changes because the component rendering the list of repositories is not routed to. Then model your machines in such a way allowing you to send messages up the chain using ReactJS' useEffect hook (or componentDidMount when using classes). The message event can trigger a state transition to start fetching or refreshing the list of repositories when the component is mounted. The machine controlling the selected user can still empty the list of repositories when it's selection changes.
If the machine should run for (most of) the life-time of the application, interpret the machine and export the service it creates. Components in need of the machine's state can import the service and use the useService hook.
A state machine's state can be persisted. See the documentation. With this knowledge you can write a version of useMachine that combined with ReactJS' useEffect hook memoizes the interpreted machine's state and re-hydrates.
The state of a state machine is just, well state. I like to think of state machines being similar to reducers in Redux. Redux could be used If you wanted to memoize and share the state of many machines in your application. When a component renders and interprets a machine it would dispatch an action with the machine's id and initial state. It could keep doing this on each transition or choose to only report back to Redux when the component unmounts. Computing or deriving data from a machine's context in other components would look exactly the same as you would currently do using selectors. Redux effectively becomes a sort of supervisor.
By far, my favourite solution so far has been combining XState with observables (RxJS). With a few years of Redux under my belt I'm the developer that will tell you: "You might not need local state" and "There's no need to put form state in local state." Switching to XState really put me through a few weeks of rethinking how to manage state in large (monolithic) web applications.
You can think of observable streams as a mini Redux store. Maybe they stream a single string, maybe a complete object with lot's of data. With RxJS you can create a stream called a BehaviourSubject that is an observable with an initial state. From there it becomes fairly trivial using combinators and operators to combine multiple streams into a single state stream. And voila, you have a single-ish state solution again.
And to take it back to XState and to state machines: Machines that produce a value, e.g. an authenticated user, a list of todos, etc, can dispatch that data to their respective observable streams. Now you have state machines controlling the logical state of your components while still being able to share data between applications and modules!
If you'd like to see it in action, I've been working on a sample repository here: https://github.com/rjdestigter/xstate-sample-kit
Thank you for perusing my thoughts! Feel free to leave a comment or add your ideas. Be kind to one another!