Skip to content
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

Merged
merged 22 commits into from
Jan 26, 2023
Merged

Conversation

davidkpiano
Copy link
Member

@davidkpiano davidkpiano commented Jan 22, 2023

This PR adds the createActorContext() helper for creating global actors:

const someMachine = createMachine({ ... });

const SomeContext = createActorContext(someMachine);

function App() {
  return (
    <SomeContext.Provider>
      <Component />
    </SomeContext.Provider>
  );
);

function Component() {
  // getting the actor instance
  const actor = SomeContext.useActorRef();

  // getting the actor state + send
  const [state, send] = SomeContext.useActor();

  // getting an actor snapshot value
  const count = SomeContext.useSelector(state => state.context.count);
  
  // ...
}

You can also provide a machine value:

// ...

function App() {
  return (
    <SomeContext.Provider value={machine.withConfig({ ... })}>
      // ...
    </SomeContext.Provider>
  );
}

@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2023

🦋 Changeset detected

Latest commit: 05d3b17

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@xstate/react Minor

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

@ghost
Copy link

ghost commented Jan 22, 2023

👇 Click on the image for a new way to code review
  • Make big changes easier — review code in small groups of related files

  • Know where to start — see the whole change at a glance

  • Take a code tour — explore the change with an interactive tour

  • Make comments and review — all fully sync’ed with github

    Try it now!

Review these changes using an interactive CodeSee Map

Legend

CodeSee Map Legend

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 22, 2023

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:

Sandbox Source
XState Example Template Configuration
XState React Template Configuration

@matthewdavi
Copy link

Why not make a contextless version using a singleton hook similar to how zustand works?

@davidkpiano
Copy link
Member Author

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);

  // ...
}

@judicaelandria
Copy link

This PR adds the createProvider() helper for creating global actors:

const someMachine = createMachine({ ... });

const SomeProvider = createProvider(someMachine);

function App() (
  <SomeProvider>
    <Component />
  </SomeProvider>
);

function Component() {
  // getting the actor instance
  const actor = SomeProvider.useContext();

  // getting the actor state + send
  const [state, send] = SomeProvider.useActor();

  // getting an actor snapshot value
  const count = SomeProvider.useSelector(state => state.context.count);
  
  // ...
}

maybe a cool feature to have, we can pass an array of machine?

const SomeProvider = createProvider([someMachine, anotherMachine]);

@davidkpiano
Copy link
Member Author

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).

@judicaelandria
Copy link

judicaelandria commented Jan 23, 2023

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>

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}

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

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.

Copy link
Member Author

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>

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

Copy link
Member

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.

@horacioh
Copy link

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

@with-heart
Copy link
Contributor

with-heart commented Jan 24, 2023

what would be de benefit on having this in the library instead of documentation on how to do it ourselves?

It's a common React pattern that many devs are familiar with but the knowledge required to create it yourself with @xstate/react isn't immediately available to you when you first start using xstate.

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

@Andarist
Copy link
Member

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

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 { default: machine }?".

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 😉

@davidkpiano
Copy link
Member Author

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).

@horacioh
Copy link

@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!

@davidkpiano
Copy link
Member Author

@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</* ??? */>(/* ??? */);

@Andarist
Copy link
Member

Andarist commented Jan 24, 2023

Traditionally, React Context is used to share state that seldom changes, whereas we're using it to share a subscribable actor.

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

@judicaelandria
Copy link

I think that most of them can usually be addressed by docs and recipes

Somewhat I completely agree with @Andarist. but @davidkpiano got a point on this too

senior developers who would struggle with the typings for doing this manually.

@with-heart
Copy link
Contributor

IMHO, people should embrace copy-pasting code more 😉

Just look at it like this PR being the copy-paste to end all copy-pastes? 😆

@davidkpiano davidkpiano changed the title [xstate/react] Add createProvider() [xstate/react] Add createActorContext() Jan 24, 2023
const actor = useInterpret(machine) as ActorRefFrom<TMachine>;
function Provider({
children,
value = machine
Copy link
Member

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.

Copy link
Member

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.

'@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:
Copy link
Member

@Andarist Andarist Jan 26, 2023

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do after release 👍

@davidkpiano davidkpiano merged commit f12248b into main Jan 26, 2023
@davidkpiano davidkpiano deleted the davidkpiano/react-provider-1 branch January 26, 2023 17:51
@github-actions github-actions bot mentioned this pull request Jan 26, 2023
This was referenced Sep 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants