-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[xstate/react] Add createActorContext()
#3778
Conversation
🦋 Changeset detectedLatest commit: 05d3b17 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
👇 Click on the image for a new way to code review
Legend |
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 05d3b17:
|
Why not make a contextless version using a singleton hook similar to how zustand works? |
Good question. We've explored that idea with @Andarist and came to the conclusion that explicitly providing through context is more robust, especially since the actors are also responsible for executing effects. But if you wanted to use a singleton hook like Zustand, it's already really easy to do so: const actor = interpret(machine).start();
function Component() {
const [state, send] = useActor(actor);
// ...
} |
maybe a cool feature to have, we can pass an array of machine? const SomeProvider = createProvider([someMachine, anotherMachine]); |
@judicaelandria Hmm interesting idea but I would recommend using multiple providers instead (for now). |
okay @davidkpiano ! but with this, we can avoid having many providers |
const actor = useInterpret(machine); | ||
|
||
return ( | ||
<ReactContext.Provider value={actor}>{children}</ReactContext.Provider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here instead of coupling with the actor
, it should be open for the user to pass in their own dependencies here.
basically value={actor}
=> value={value?? actor}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but the dependency is the actor! what other value can we pass here?
and also, in this case, it's exactly like react context
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this behavior will close the door to do the dependency injection which is an excellent feature from react context to decouple the dependency and UI so that the UI can be reused with the different instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're thinking of this usage?
// another actor of same type
<SomeProvider value={anotherActor}>
// ...
</SomeProvider>
And also:
// another machine of same type
<SomeProvider value={anotherMachine}>
// ...
</SomeProvider>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're thinking of this usage?
// another actor of same type <SomeProvider value={anotherActor}> // ... </SomeProvider>And also:
// another machine of same type <SomeProvider value={anotherMachine}> // ... </SomeProvider>
exactly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like something that we'd like to support. But on the other hand - it would be nice to understand when exactly you'd like to use it. It's also an additive feature - so it could be added later on.
what would be de benefit on having this in the library instead of documentation on how to do it ourselves? I do have my abstractions for this on my apps, as others too, and maybe this is enough? I see the potential for sure!, not sure why have it in the actual library is my point |
It's a common React pattern that many devs are familiar with but the knowledge required to create it yourself with So there's this disconnect where a pattern that should be easy for you because you do it often in React isn't because you aren't familiar with how to do it with this particular library. David mentioned this after the Stately hackathon was over on Friday and it was definitely a lesson learned directly from that experience. A lot of teams were able to move quickly on a lot of fronts but they found that they weren't able to figure out how to make their machines global. This allows the library to solve that particular problem up front as part of the API rather than making us all learn and then repeat basically the same pattern all over the place |
I very much share this sentiment. This PR introduces something that can easily be put together in the user land. It also already shows how this kind of a thing immediately starts some feature bikeshedding disputes - "can you support arrays?", "can you support objects with named keys but also accept a single machine, effectively making that a sugar for Those are not bad ideas. The problem is that those are things that some individuals want and not things that we should have opinions about. The existing hooks are simple, flexible and composable - as they should be. Thin abstractions are expected to be built on top of those building blocks. That being said... there is a clear need in the community for a solution like this. People are often a little bit scared of introducing custom "helpers" like this in their codebases. I think that overall, it doesn't cause any harm to have this in place here - kinda as a reference implementation, one that could be extended further by just copy-pasting it into the given project and modifying it to somebody's needs. @with-heart raises a lot of good points but, at the same time, I think that most of them can usually be addressed by docs and recipes. It's not exactly that providing an API without any docs allows people to discover a helper like this easily. I like how React Redux is documenting how to build things that are commonly requested: https://react-redux.js.org/api/hooks#hooks-recipes . IMHO, people should embrace copy-pasting code more 😉 |
Somewhat agree, but admittedly this is a problem for two audiences: junior developers who might not have as much experience with React Context (which is not an intuitive API and has a slight learning curve) as well as senior developers who would struggle with the typings for doing this manually. I strongly believe that it is much more helpful than not to provide a "blessed" and optional way to do something than to not provide it at all, especially if the maintenance cost is low (as it is here - this is not a complex implementation). |
@davidkpiano about the first audience you mentioned(junior devs): Do you find it common that junior devs understand first state machines and don't have much experience with the React Context API? and about the second audience (senior devs) Do you believe that they would prefer a "baked in solution" if the actual code implementation is 50-60 LOC? (my implementation is 50) I don't have too much context on a broad range of developers, that's why I'm curious! |
@horacioh Even if they understand both, it's wiring them together that can get confusing. Traditionally, React Context is used to share state that seldom changes, whereas we're using it to share a subscribable actor. The temptation is to do this: const [state, send] = useMachine(machine);
return <SomeContext.Provider value={state} />
// or...
return <SomeContext.Provider value={{state, send}} /> which can introduce bad performance. And even if you know to provide the actor itself, this is not intuitive: const SomeContext = React.createContext(/* what goes here?? */);
// and worse in TypeScript:
const SomeContext = React.createContext</* ??? */>(/* ??? */); |
I would say that traditionally context was used to inject all sorts of things (like a Redux store, history object for routers etc) - it feels like a newer "trend" that people started to using it as state management solution ;p |
Somewhat I completely agree with @Andarist. but @davidkpiano got a point on this too
|
Just look at it like this PR being the copy-paste to end all copy-pastes? 😆 |
createProvider()
createActorContext()
const actor = useInterpret(machine) as ActorRefFrom<TMachine>; | ||
function Provider({ | ||
children, | ||
value = machine |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When it comes to the barebones React context value
is both what you provide and what you consume. We break that parity here because we provide an actor ref under the hood but the value
here is the machine.
I'd probably name this prop a machine
for this reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OTOH, we already "break" the parity with the barebones React context as the "initial value" used to create this is different from what we end up providing.
But even considering this - I feel like a more descriptive prop name (machine
) is better. React has to use a generic value
because their whole signature is super generic and has to accept just any value.
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
76496be
to
679814d
Compare
'@xstate/react': minor | ||
--- | ||
|
||
The `createActorContext(...)` helper has been introduced to make global actors easier to use with React. It outputs a React Context object with the following properties: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also find its way to the README
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do after release 👍
This PR adds the
createActorContext()
helper for creating global actors:You can also provide a machine value: