Skip to content

Commit

Permalink
[xstate/react] Add createActorContext() (#3778)
Browse files Browse the repository at this point in the history
* Add createProvider

* Fix types

* Update packages/xstate-react/src/createProvider.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Tweak types within `createProvider` (#3781)

* createProvider -> createActorContext

* Allow `compare` to be passed to the `useSelector` (#3782)

* Use `screen` API for accessing elements in the newly added test file

* Update packages/xstate-react/src/createActorContext.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* .useContext -> .useActorRef

* Code juggle `createActorContext` a little bit (#3785)

* Switch back useActorRef -> useContext, add value

* Update packages/xstate-react/src/createActorContext.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Add tests for context hooks being used outside of the Provider (#3787)

* Update packages/xstate-react/src/createActorContext.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-react/src/createActorContext.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/xstate-react/test/createActorContext.test.tsx

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Fix lint issues and rename useContext -> useActorRef

* Remove redundant `any`

* Fix hook names

* fixed expectations in tests

* Add changeset

* Export createActorContext

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Jan 26, 2023
1 parent c7cd5aa commit f12248b
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 6 deletions.
41 changes: 41 additions & 0 deletions .changeset/tender-dragons-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@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:

- `.Provider` - The React Context provider
- `.useActor(...)` - A hook that can be used to get the current state and send events to the actor
- `.useSelector(...)` - A hook that can be used to select some derived state from the actor's state
- `.useActorRef()` - A hook that can be used to get a reference to the actor that can be passed to other components

Usage:

```jsx
import { createActorContext } from '@xstate/react';
import { someMachine } from './someMachine';

// Create a React Context object that will interpret the machine
const SomeContext = createActorContext(someMachine);

function SomeComponent() {
// Get the current state and `send` function
const [state, send] = SomeContext.useActor();

// Or select some derived state
const someValue = SomeContext.useSelector((state) => state.context.someValue);

// Or get a reference to the actor
const actorRef = SomeContext.useActorRef();

return (/* ... */);
}

function App() {
return (
<SomeContext.Provider>
<SomeComponent />
</SomeContext.Provider>
);
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"lint-staged": "^8.2.1",
"markdown-it-codesandbox-embed": "^0.1.0",
"patch-package": "^6.2.2",
"prettier": "^2.0.2",
"prettier": "^2.8.3",
"spawn-command": "0.0.2-1",
"ts-jest": "^26.5.6",
"tslib": "^2.3.1",
Expand Down
70 changes: 70 additions & 0 deletions packages/xstate-react/src/createActorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import { useInterpret } from './useInterpret';
import { useActor as useActorUnbound } from './useActor';
import { useSelector as useSelectorUnbound } from './useSelector';
import { ActorRefFrom, AnyStateMachine, EmittedFrom } from 'xstate';

export function createActorContext<TMachine extends AnyStateMachine>(
machine: TMachine
): {
useActor: () => ReturnType<typeof useActorUnbound<ActorRefFrom<TMachine>>>;
useSelector: <T>(
selector: (snapshot: EmittedFrom<TMachine>) => T,
compare?: (a: T, b: T) => boolean
) => T;
useActorRef: () => ActorRefFrom<TMachine>;
Provider: (props: {
children: React.ReactNode;
machine?: TMachine | (() => TMachine);
}) => React.ReactElement<any, any>;
} {
const ReactContext = React.createContext<ActorRefFrom<TMachine> | null>(null);

const OriginalProvider = ReactContext.Provider;

function Provider({
children,
machine: providedMachine = machine
}: {
children: React.ReactNode;
machine: TMachine | (() => TMachine);
}) {
const actor = useInterpret(providedMachine) as ActorRefFrom<TMachine>;

return <OriginalProvider value={actor}>{children}</OriginalProvider>;
}

Provider.displayName = `ActorProvider(${machine.id})`;

function useContext() {
const actor = React.useContext(ReactContext);

if (!actor) {
throw new Error(
`You used a hook from "${Provider.displayName}" but it's not inside a <${Provider.displayName}> component.`
);
}

return actor;
}

function useActor() {
const actor = useContext();
return useActorUnbound(actor);
}

function useSelector<T>(
selector: (snapshot: EmittedFrom<TMachine>) => T,
compare?: (a: T, b: T) => boolean
): T {
const actor = useContext();
return useSelectorUnbound(actor, selector, compare);
}

return {
Provider,
useActorRef: useContext,
useActor,
useSelector
};
}
1 change: 1 addition & 0 deletions packages/xstate-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { useInterpret } from './useInterpret';
export { useSelector } from './useSelector';
export { useSpawn } from './useSpawn';
export { shallowEqual } from './utils';
export { createActorContext } from './createActorContext';

0 comments on commit f12248b

Please sign in to comment.