-
-
Notifications
You must be signed in to change notification settings - Fork 622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: jotai/xstate #289
feat: jotai/xstate #289
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 2a44c15:
|
A toggle example: https://codesandbox.io/s/react-typescript-forked-pmy7c?file=/src/App.tsx |
It's a really genius idea, could we merge the createMachine with the atom itself? |
You mean passing machine config instead of machine itself. (yeah, like we do with |
https://codesandbox.io/s/react-typescript-forked-0obdb?file=/src/App.tsx |
https://xstate.js.org/docs/packages/xstate-react/
They have four more apis other than |
@dai-shi Can you support const [state, send] = useAtom(...);
// ...
send('SOME_EVENT', { value: 'whatever' }) |
@davidkpiano const [state, send] = useAtom(...);
send(['SOME_EVENT', { value: 'whatever' }]) Is this acceptable? Of course, one could create a custom hook, if they want. const useMachineAtom = (machineAtom) => {
const [state, send] = useAtom(machineAtom)
return [state, useCallback((...args) => send(args), [send])]
} |
Interesting, but no worries. The |
How can we support https://xstate.js.org/api/classes/interpreter.html#send
We need to pass the payload, correct? |
The payload gets passed in as part of the object: send({ type: 'SOME_EVENT', value: 'something' }) |
So, does this look correct? const { type, ...payload } = eventWithPayload
service.send(type, payload) |
Hm, looking at your example again, https://codesandbox.io/s/react-typescript-forked-7hyg9?file=/src/App.tsx |
Here's my modified example: https://codesandbox.io/s/react-typescript-forked-kjmk8?file=/src/App.tsx const defaultTextAtom = atom("edit me");
const editableMachineAtom = atomWithMachine((get) =>
createEditableMachine(get(defaultTextAtom))
); We can get some values from atoms to feed into machine creation. |
Okay, confirmed. |
This reverts commit 24a679a.
Although what's ready is minimal, I suppose it's good for the initial release. |
Right - feeding the values for a lazy init is OK. I've meant that beyond that it's possible to access atom values after that there using
If we are talking only about machines - they are kinda the same. A service could be of a different type but what is returned from Overall it looks good and the ability to share a machine atom between components easily is very nice. All of the consuming components will get subscribed to full state changes and in certain scenarios, this might be a perf issue. So I would try to figure out what is the best selector-like strategy for this (or maybe some proxy-based reactivity stuff - this seems to be your specialty 😉 ) |
Got it. The design doesn't allow this, but the current code doesn't prevent the misusage. I will add a guard.
Thanks. It requires some more time to learn these new things. So far, what I see is:
(still confused with actors...)
To address it, jotai already has so-called derived atoms. const machineAtom = atomWithMachine(...)
const fooOfMachineAtom = atom((get) => get(machineAtom).foo) We have Thanks for your feedback. I will release this shortly, but please let us know if you notice anything and we can follow up. |
Great work, but I'm a little lost as to how to pass the machine options... It seems that However, these options are more often than not used to set the state machine services and actions, which perform the actual tasks (side effects). As such, they can be (and often are) asynchronous functions. Nowadays in React world, these are created using hooks as well. Say, So I'm struggling to see how you can construct the atom. In the codesandbox example:
the above code is happening outside a React component, so you can't have anything with hooks passed in the machine options, or you would get the dreaded " Invalid hook call. Hooks can only be called inside of the body of a function component"... Am I missing something? |
Hi, good to see someone trying it.
How would you use those options, if you were to use |
@dai-shi the first argument to The wiring, what is actually being done, for example calling a service, a REST endpoint, etc, is usually passed as the second argument to This provides some decoupling, should you decide one day to change |
Thanks for clarification. Helps to get the general idea. So, the reason you would want to pass Now, if you assume useAtom+atomWithMachine is a replacement for useContext+useMachine, how would you solve your use case with useContext and single useMachine?
Do you have any example we can discuss with a little concrete case? Doesn't have to be real, but a hypothetical/pseudo example. |
The reason people pass options to To be honest, I'm not entirely sure I understand what you are doing in const ServiceContext = React.createContext();
const App = ({ children }) => {
const [_state, _send, service] = useMachine(someMachine);
return (
<ServiceContext.Provider value={service}>
{children}
</ServiceContext.Provider>
);
}
// in a child...
const Child = () => {
const service = React.useContext(ServiceContext);
const [state, send] = useService(service);
// ...
} As for any example involving hooks. Essentially picture the fact that xstate side effects and actions triggered when you enter a state are often asynchronous functions that query some third party server for example. Now unfortunately, because of the React hooks craze, many of the popular "fetch"-style libraries like React Query, or React Apollo GraphQL now offer their React API as hooks. Meaning, you have to be inside a component when you essentially create the wiring between your app UI and a third-party service. The result of that hook is both the means to get the data, and the data itself once the async function under the hood has resolved, and that is what a UI component will use to display its contents. So you see the issue here; all the Jotai examples feature atoms being created outside a component using I also found an example for your perusal. import { useMachine } from '@xstate/react'
...other code
const [state, send] = useMachine(usersMachine, {
// This is where we pass in the "fetchUsers" function that we
// referenced in the machine configuration
services: {
fetchUsers: () => fetch('some-endpoint')
.then((res) => res.json())
// When we resolve the promise here, we'll trigger a state transition
// to the "success" state
.then((res) => Promise.resolve(res.data))
.catch((err) =>
// When we reject the promise here, we'll trigger a state transition
// to the "error" state
Promise.reject({
status: err.response.status,
data: err.response.data
})
)
}
})
const isLoading = state.matches('loading')
const isSuccess = state.matches('success')
const isError = state.matches('error')
const handleButtonClick = () => {
send('FETCH')
}
...other code Now you can see the second argument to Hope this helps. |
Thanks. I think I'm getting to understand what you are trying to say.
Please note
The goal is to have a machine in the atom world.
so, like you said, your assumption is correct. please note that atom defined outside components, will run inside a component. but you are right. there's no way to interact with other state/props/hooks than atoms.
Yeah, this one works.
But, not for others. I agree the current
Meanwhile, I would like to ask a question: How would you use RQ's useQuery with this old React context style? const ServiceContext = React.createContext();
const App = ({ children }) => {
const [_state, _send, service] = useMachine(someMachine);
return (
<ServiceContext.Provider value={service}>
{children}
</ServiceContext.Provider>
);
}
// in a child...
const Child = () => {
const service = React.useContext(ServiceContext);
const [state, send] = useService(service);
// ...
// Do you mean we can do something here with useQuery()??
} |
Oh very easy, you are inside a component, you can create as many hooks as you want. For example, here is a hook I wrote yesterday that I call, instead of calling import { gql, useMutation } from '@apollo/client';
import { createMachine } from 'xstate';
const START_TIMER_MUTATION = gql`...`
const STOP_TIMER_MUTATION = gql`...`
const timeChargingMachine = createMachine(
{
...
states: {
...
STARTING_TIMER: {
invoke: {
src: 'startTimer',
...
},
STOPPING_TIMER: {
invoke: {
src: 'stopTimer',
...
}
});
export function useTimeChargingStateMachine() {
const [startTimerMutation] = useMutation(START_TIMER_MUTATION);
const [stopTimerMutation] = useMutation(STOP_TIMER_MUTATION);
const machineOptions = {
services: {
startTimer: (ctx, event) =>
startTimerMutation({
variables: { id: event.timer.id },
}).then(({ data }) => (data.startTimer.userErrors !== null ? Promise.reject(data) : data)),
stopTimer: (ctx, event) =>
stopTimerMutation({
variables: { id: event.timer.id },
}).then(({ data }) => (data.stopTimer.userErrors !== null ? Promise.reject(data) : data)),
},
};
return useMachine(timeChargingMachine, machineOptions);
} Note how the machine options take care of binding the Then in my This might be the smaller, visible part of the iceberg I'm afraid. If your atom is meant to be shared across UI components... then it is possible each component will want to have its own side effects (aka actions). This is currently not possible because the options are specified when calling |
Hmm, this is done in the outside of Context Provider? I thought you want it in the
Okay, one machine instance and multiple services connected to the machine, right?
Hm, so services are separate instances? Anyway, thanks for all your help. I wish there would be a small codesandbox to play with. If there were something with useMachine, I could try converting it to (maybe modified) atomWithMachine. |
No, this is done inside. It's exactly the same example as I gave you, except I replaced const App = ({ children }) => {
const [_state, _send, service] = useTimeChargingStateMachine();
return (
<ServiceContext.Provider value={service}>
{children}
</ServiceContext.Provider>
);
}
...
No :) To quote xstate author's:
Calling While a state machine/statechart with a pure .transition() function is useful for flexibility, purity, and testability, in order for it to have any use in a real-life application, something needs to:
The interpreter is responsible for interpreting the state machine/statechart and doing all of the above - that is, parsing and executing it in a runtime environment. An interpreted, running instance of a statechart is called a service. The service is what you listen to using XState's Anyway, glad it helps. I'm sure |
Good. I meant this
Yeah, I saw that.
This is good to know. So, what I called a instance is s a service.
I see the whole point of your suggestion is this. And this is not only about So, the fact is, when we need to create an atom that depends on props, define it in component. const Component = ({ id }) => {
const dataAtom = useMemo(() => atom({ id }), [id])
const [data, setData] = useAtom(dataAtom)
// ...
} It's be the same for |
Correct, I think we found the crux of the issue here.
Wow, I never thought of using I think if you updated the doc to mention that specific use case, people who are familiar with React hooks would put two and two together and see the benefit right away, since they could use Thanks for the tip |
Very good. I will see how I can update the docs. I would love to create a codesandbox example, a simple one. Maybe an artificial one that updates a state from useState on xstate effects. Thanks for your help. |
|
True in this case. Thanks for the catch. |
This is to add new lib
jotai/xstate
with xstate.atomWithMachine
implementationNote this PR is for the basic use case. It doesn't cover advanced use cases yet.