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

Refactor @xstate/react to useSyncExternalStore #2939

Merged
merged 36 commits into from
Apr 6, 2022
Merged

Refactor @xstate/react to useSyncExternalStore #2939

merged 36 commits into from
Apr 6, 2022

Conversation

Andarist
Copy link
Member

@Andarist Andarist commented Jan 13, 2022

closes #3196
closes #2821

@changeset-bot
Copy link

changeset-bot bot commented Jan 13, 2022

🦋 Changeset detected

Latest commit: 0268477

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

This PR includes changesets to release 5 packages
Name Type
@xstate/react Major
xstate Patch
@xstate/fsm Major
@xstate/svelte Major
@xstate/vue Major

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 13, 2022

CodeSee Review Map:

Review these changes using an interactive CodeSee Map

Review in an interactive map

View more CodeSee Maps

Legend

CodeSee Map Legend

…g back caching of the `service.initialState`
Comment on lines 77 to 78
"react": "18.0.0-rc.0-next-811634762-20220110",
"react-dom": "18.0.0-rc.0-next-811634762-20220110",
Copy link
Member Author

Choose a reason for hiding this comment

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

Before landing this we should set up tests to run against both React 17 and React 18. It's totally doable - I just didn't bother to do it right now.

This should also only land after React 18 gets finally released.

Copy link
Member Author

Choose a reason for hiding this comment

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

OTOH - I don't think now that this would add much value here. Stuff that works with React 18 should just work with React 17. So it might be enough to just test with React 18. We can always also revisit adding extra tests for React 17 in the future.

@@ -36,18 +35,15 @@ function toObserver<T>(
};
}

export function useInterpret<
export function useIdleInterpreter<
Copy link
Member Author

Choose a reason for hiding this comment

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

Not the best name... but naming things is hard™ and naming some hooks is even harder 😅

const service = useInterpret(getMachine, options, listener);
useEffect(() => {
const rehydratedState = options.state;
service.start(
Copy link
Member Author

Choose a reason for hiding this comment

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

After those changes, useMachine no longer depends on the useInterpret. useInterpret starts the machine in the layout effect but this one has to do that in the passive effect. Maybe we should change the behavior of the useInterpret too - but I've wanted to avoid changing too much now. I've also wanted to change this for now because how the listener/subscriber gets wired is now different and has to be managed here, in the useMachine, but it was previously managed by useInterpret. Technically, we could just not pass any subscriber to the useInterpret and just subscribe with useSyncExternalStore here... and maybe that is something that we should do.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I've added a comment on why we can't use useInterpret here right now:
47f566c#diff-19f47c3f58ef51d8a79a32343f5c32c02d6d7237ecc29f636cb53846d00424d5R68-R69

const storeSnapshot = useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
undefined,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is an argument slot for getServerSnapshot. I'm not yet sure how this should work... maybe we should just use the initialState for it? It somewhat doesn't matter for useMachine because nothing should be able to change its state between render and the commit phase but all the other hooks are also using the useSyncExternalStore stuff and the same cannot be said about them - so we should think how this should be handled there.

[]
[service]
);
const storeSnapshot = useSyncExternalStoreWithSelector(
Copy link
Member Author

Choose a reason for hiding this comment

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

useMachine kinda shouldn't have to use useSyncExternalStoreWithSelector - it could use the simpler useSyncExternalStore, but to make this possible we'd have to have immutable states without .changed

# Conflicts:
#	packages/xstate-react/src/index.ts
#	packages/xstate-react/src/useActor.ts
#	packages/xstate-react/src/useInterpret.ts
#	packages/xstate-react/src/useMachine.ts
#	packages/xstate-react/src/useReactEffectActions.ts
#	packages/xstate-react/test/useMachine.test.tsx
#	packages/xstate-react/test/useService.test.tsx
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 27, 2022

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 0268477:

Sandbox Source
XState Example Template Configuration
XState React Template Configuration

# Conflicts:
#	.changeset/red-ants-lick.md
#	packages/xstate-react/package.json
#	packages/xstate-react/src/useInterpret.ts
#	packages/xstate-react/src/useMachine.ts
#	packages/xstate-react/test/useActor.test.tsx
#	packages/xstate-react/test/useMachine.test.tsx
#	packages/xstate-react/test/useSelector.test.tsx
@koenoe
Copy link

koenoe commented Apr 1, 2022

Nice!!

}
};
}, [service]);
const isEqual = useCallback(
Copy link
Member

Choose a reason for hiding this comment

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

Can this just be declared externally?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, yes - this one here, for /fsm, doesn't require access to the local service - so it can be hoisted

good 👁️ 👍

@@ -462,4 +390,150 @@ describe('interpreter', () => {

expect(nextState.actions.map((a) => a.type)).toEqual(['action']);
});

describe('`start` method', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

this block has just been moved to the bottom as tests were sort of mixed and the order in the file didn't match the order in the test report

if (options) {
(service as any)._machine._options = options;
}
});

const useServiceResult = useService(service);

useEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

after those changes @xstate/fsm should be nicely compatible with StrictMode and Fast Refresh (something that can't be said about the core 😬 )

We still can't use the "incoming" machine config from the Fast Refresh though but at least we don't display outdated state after Fast Refresh nor we don't have to fully restart the service so the state is preserved.

const initialStateChanged =
nextState.changed === undefined &&
(Object.keys(nextState.children).length > 0 ||
typeof prevState.changed === 'boolean');
Copy link
Member Author

Choose a reason for hiding this comment

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

this additional condition here allows us to "restart" to the initial state after Fast Refresh, whereas previously we were displaying the outdated state that was no longer true for the current, restarted, service


const { rerender } = render(<App value={1} />);

expect(actual).toEqual(suiteKey === 'strict' ? [1, 1] : [1]);
Copy link
Member

Choose a reason for hiding this comment

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

Sigh...

Copy link
Member

@davidkpiano davidkpiano left a comment

Choose a reason for hiding this comment

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

🚢 ⚛️ 1️⃣ 8️⃣

Co-authored-by: David Khourshid <davidkpiano@gmail.com>
@Andarist Andarist merged commit 4d1ce75 into main Apr 6, 2022
@Andarist Andarist deleted the react18 branch April 6, 2022 12:49
@github-actions github-actions bot mentioned this pull request Apr 6, 2022
nevilm-lt pushed a commit to nevilm-lt/xstate that referenced this pull request Apr 22, 2022
Refactor `@xstate/react` to useSyncExternalStore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants