-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Support context bridging with opaque store #298
Conversation
Hi @inlet! Thank you for your pull request and welcome to our community.We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks! |
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
1 similar comment
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
@inlet - We don't want to expose the internal store as part of the API as it may change. It also contains React component subscriptions that wouldn't be appropriate outside the context of a |
Ideally Contexts are shared between renderers, but unfortunately they're not. See #140 Custom renderers (like React ART, React Threejs Fiber, React Pixi, etc) are running an isolated reconciler. Often they are initiated from ReactDOM, but the problem is that the parent Context isn't propagated to the children in these custom renderers. For instance: // Stage initiates a custom renderer
// Text is a component rendered in this custom renderer
import { Stage, Text } from '@inlet/react-pixi';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { theme } from './atoms';
const PixiText = () => {
// this component is rendered in a custom renderer
//!!! theme cannot be set as it cannot reach the recoil context
const theme = useRecoilValue(theme);
return <Text text={theme.color} />
}
const Drawing = () => {
// theme is set here, as it can reach the context
const theme = useRecoilValue(theme);
// return a custom renderer
// in this case react pixi
return (
<Stage>
<PixiText />
</Stage>
);
}
const App = () => (
<RecoilRoot>
<Drawing />
</RecoilRoot>
);
export default App;
I'm not familiar with the Snapshot API, is there any docs about this? Does it serialize data? this can have a huge performance impact only for passing through context. Looking forward to your reply, thanks! |
@inlet - It hasn't been published yet, but we're getting close. I hope to understand the requirements for your use-case to see if they might be able to be covered as well. Snapshots do not require serialization. It might work to insert a nested controlled |
Thanks for your fast reply! If it rerenders on every state change then I think this isn't a really good solution as it does impact the performance. The underlying problem is React not passing through the context or at least have a way to pass it through. Because of this limitation every custom renderer uses a Context bridge to pass through the context to the renderer. That's why exposing the actual context isn't such a bad idea. Anyway I'm looking forwards to a decent implementation as I love Recoil's approach of state management. Thanks! |
Ok, thanks for explaining. Give us a chance to discuss this more.
Get Outlook for iOS<https://aka.ms/o0ukef>
…________________________________
From: Patrick Brouwer <notifications@github.com>
Sent: Thursday, June 11, 2020 1:41:29 AM
To: facebookexperimental/Recoil <Recoil@noreply.github.com>
Cc: Douglas Armstrong <dougarmstrong@fb.com>; Review requested <review_requested@noreply.github.com>
Subject: Re: [facebookexperimental/Recoil] Expose the `RecoilContext` (#298)
Thanks for your fast reply!
If it rerenders on every state change then I think this isn't a really good solution as it does impact the performance.
The underlying problem is React not passing through the context or at least have a way to pass it through.
Because of this limitation every custom renderer uses a Context bridge to pass through the context to the renderer. That's why exposing the actual context isn't such a bad idea.
Anyway I'm looking forwards to a decent implementation as I love Recoil's approach of state management.
Thanks!
—
You are receiving this because your review was requested.
Reply to this email directly, view it on GitHub<#298 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/ABDVJ2HFJRW6YJVKUNMEQ2TRWCKDTANCNFSM4N2PKWYQ>.
|
Welcome, and absolutely! Please reach out if I can be of any help. Thanks |
67734cc
to
ad4d594
Compare
We really don't want to expose the internals of the Recoil store in the API. But, would the following work to support context bridging and would you be willing to update the PR? Add a hook or something like |
Sure I can update the PR. Exporting a hook
Why the need of adding a prop on Here's the implementation of a // the context bridge
export function ContextBridge({ children, Context, render }) {
return (
<Context.Consumer>
{(value) => render(
<Context.Provider value={value}>{children}</Context.Provider>
)}
</Context.Consumer>
);
};
// usage
export function PixiStage({ children, ...props }) {
const RecoilContext = useRecoilContext();
return (
<ContextBridge
Context={RecoilContext}
render={children => (<Stage {...props}>{children}</Stage>)}
>
{children}
</ContextBridge>
);
}
); |
Or do you mean that Please let me know what your thoughts are, thanks! |
The idea is that whatever this |
51d96e1
to
a586175
Compare
I've updated the PR and it now works with a store which you can pass down to import { atom, RecoilRoot, useRecoilStore_UNSTABLE } from 'recoil';
import { Stage, Text } from '@inlet/react-pixi';
const counterState = atom({
key: 'counter',
default: 0,
});
const PixiText = () => {
// runs in custom renderer!
const value = Recoil.useRecoilValue(counterState);
return <ReactPixi.Text text={value} />;
};
const PixiStage = () => {
// fetch the parent RecoilRoot store
const store = useRecoilStore_UNSTABLE();
// and pass it down to the next RecoilRoot
return (
<Stage>
<RecoilRoot store={store}>
<PixiText />
</RecoilRoot>
</Stage>
);
};
|
FYI the tests are failing on the master branch.. not related to this PR. |
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.
Please also add unit tests in /src/hooks/__tests__
src/hooks/Recoil_Hooks.js
Outdated
@@ -688,6 +688,10 @@ function useRecoilCallback<Args: $ReadOnlyArray<mixed>, Return>( | |||
); | |||
} | |||
|
|||
function useRecoilStore() { |
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.
The key is that this returns an opaque type. So, we need to define a new Flow type that this returns that is defined to be "opaque
" (and uses an equivalent technique for the TypeScript type to be opaque)
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.
Can you elaborate on the “opaque” type? This function now returns the actual Store
type, which is desired right? Can you create a suggestion?
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 mean something like this for Flow: https://flow.org/try/#0FAFwngDgpgBAyiA9gJ1gXhgb2DXMBmiiAXDAOQBGAhsmQDTAC+A3MMIhFQI4Cus40GACUoAY0QBLADYIU6eElStg+HgDtRICYjUweAZygjx02agAUASlLHJMxbGx4YqED2S7MhEuWq0WTEA
And this for TypeScript:
https://www.typescriptlang.org/play/#code/CYUwxgNghgTiAEYD2A7AzgF3gJXEglhAMoZJwD6A8gAoCCAigKoCiAXPAK4r4COHCaAJ4BbAEZIIAbgBQIAB4AHMlnwoMIGADMoYBLmSESZBAG9p8eHCjBUEQfADa+gsVIUaDFgF12GGPxkAXyA
@@ -103,6 +104,7 @@ module.exports = { | |||
useRecoilTransactionObserver_UNSTABLE: useRecoilTransactionObserver, | |||
useTransactionObservation_UNSTABLE: useTransactionObservation_DEPRECATED, | |||
useSetUnvalidatedAtomValues_UNSTABLE: useSetUnvalidatedAtomValues, | |||
useRecoilStore_UNSTABLE: useRecoilStore, |
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.
Also need to update the TypeScript definitions.
Thanks for updating the PR! Yeah, there should be one known failure that will be resolved with some changes we have in the pipeline.. sorry about that.. |
src/core/Recoil_RecoilRoot.react.js
Outdated
@@ -42,6 +42,7 @@ type Props = { | |||
setUnvalidatedAtomValues: (Map<string, mixed>) => void, | |||
}) => void, | |||
initializeState?: MutableSnapshot => void, | |||
store?: Store, |
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.
Also label this as store_UNSTABLE
Can you please collaborate on this PR as you clearly have a better understanding what you want. Thanks! |
Sure, I can update the types easily enough after we export the PR, but please add a unit test. |
I've added unit tests, if you could add the types that would be 👌 |
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.
@drarmstr has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
Summary: Export RecoilRoot's store via an opaque type to make Context bridging possible. Recoil uses `React.Context` internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem. Here's an example of a Context bridge I use for ReactPixi: ``` import { Stage, Text } from 'inlet/react-pixi'; import { RecoilContext, useSetRecoilValue } from 'recoil'; import { theme } from './atoms'; const ContextBridge = ({ children, Context, render }) => { return ( <Context.Consumer> {(value) => render(<Context.Provider value={value}>{children}</Context.Provider>)} </Context.Consumer> ); }; const RendererComponent = () => { const theme = useSetRecoilValue(theme); return <Text text={theme.settings} /> } const App = () => ( <ContextBridge Context={RecoilContext} render={(children) => <Stage>{children}</Stage>} > <RendererComponent /> </ContextBridge> ); ``` Proposed API for Recoil to support context briding without exposing the internals of the Recoil store: ``` function RenderingBridge({children}) { const store = useRecoilStore_UNSTABLE(); return ( <CustomRenderer> <RecoilRoot store_UNSTABLE={store}>{children}</RecoilRoot> </CustomRenderer> ); } function MyApp() { return ( <RecoilRoot> ... <RenderingBridge> ... </RenderingBridge> </RecoilRoot> ); } ``` Fixes facebookexperimental#140 Pull Request resolved: facebookexperimental#298 Differential Revision: D22929302 Pulled By: drarmstr fbshipit-source-id: c54d2b2fd12eecca97385428d9cd449525d71632
@inlet - I imported this PR to our repository and made the following changes:
However, this diff is having a conflict with the new support for concurrent mode. That diff checks if React's Unfortunately, with bypassing I also put up a diff (PR #512) with an alternative API that might be cleaner for supporting context bridging without exposing internals. We'll discuss that more amongst the team. But, it also suffers from the same problem of the failing test. I'm not quite sure how to export my changes to this PR back to GitHub, but you can see them if you look at the individual commits for #512. @inlet, can you figure out why the context bridging test is failing when the atom is set? |
Summary: Export RecoilRoot's store via an opaque type to make Context bridging possible. Recoil uses `React.Context` internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem. Here's an example of a Context bridge I use for ReactPixi: ``` import { Stage, Text } from 'inlet/react-pixi'; import { RecoilContext, useSetRecoilValue } from 'recoil'; import { theme } from './atoms'; const ContextBridge = ({ children, Context, render }) => { return ( <Context.Consumer> {(value) => render(<Context.Provider value={value}>{children}</Context.Provider>)} </Context.Consumer> ); }; const RendererComponent = () => { const theme = useSetRecoilValue(theme); return <Text text={theme.settings} /> } const App = () => ( <ContextBridge Context={RecoilContext} render={(children) => <Stage>{children}</Stage>} > <RendererComponent /> </ContextBridge> ); ``` Proposed API for Recoil to support context briding without exposing the internals of the Recoil store: ``` function RenderingBridge({children}) { const store = useRecoilStore_UNSTABLE(); return ( <CustomRenderer> <RecoilRoot store_UNSTABLE={store}>{children}</RecoilRoot> </CustomRenderer> ); } function MyApp() { return ( <RecoilRoot> ... <RenderingBridge> ... </RenderingBridge> </RecoilRoot> ); } ``` Fixes facebookexperimental#140 Pull Request resolved: facebookexperimental#298 Differential Revision: D22929302 Pulled By: drarmstr fbshipit-source-id: e6cbb1c1f6cf3f2bf92a070719a2b71b504d17ea
@inlet has updated the pull request. You must reimport the pull request before landing. |
@drarmstr Thanks for all the effort! First of all, I'd love to collaborate on this with you to work out a solid solution, however I'm very limited in time and won't be able to work on this on short term. Luckily the fix is almost there.. given that you have a better mental model of this codebase (and the new React concurrent mode) + you are the core contributor of the project makes you more suitable to solve the last piece of the puzzle 😄 Thanks! |
@inlet I don't have the bandwidth to explore with an actual nested renderer using We're also currently thinking of using #516 for the API |
Summary: NOTE: Please note updated context bridging proposal in D22983479 or D22935480 Export RecoilRoot's store via an opaque type to make Context bridging possible. Recoil uses `React.Context` internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem. Here's an example of a Context bridge I use for ReactPixi: ``` import { Stage, Text } from 'inlet/react-pixi'; import { RecoilContext, useSetRecoilValue } from 'recoil'; import { theme } from './atoms'; const ContextBridge = ({ children, Context, render }) => { return ( <Context.Consumer> {(value) => render(<Context.Provider value={value}>{children}</Context.Provider>)} </Context.Consumer> ); }; const RendererComponent = () => { const theme = useSetRecoilValue(theme); return <Text text={theme.settings} /> } const App = () => ( <ContextBridge Context={RecoilContext} render={(children) => <Stage>{children}</Stage>} > <RendererComponent /> </ContextBridge> ); ``` Proposed API for Recoil to support context briding without exposing the internals of the Recoil store: ``` function RenderingBridge({children}) { const store = useRecoilStore_UNSTABLE(); return ( <CustomRenderer> <RecoilRoot store_UNSTABLE={store}>{children}</RecoilRoot> </CustomRenderer> ); } function MyApp() { return ( <RecoilRoot> ... <RenderingBridge> ... </RenderingBridge> </RecoilRoot> ); } ``` Fixes facebookexperimental/Recoil#140 Pull Request resolved: facebookexperimental/Recoil#298 Reviewed By: davidmccabe Differential Revision: D22929302 Pulled By: drarmstr fbshipit-source-id: 41c6d8d66d9326da0e1d09583d98934f60ab6ba2
Summary: NOTE: Please note updated context bridging proposal in D22983479 or D22935480 Export RecoilRoot's store via an opaque type to make Context bridging possible. Recoil uses `React.Context` internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem. Here's an example of a Context bridge I use for ReactPixi: ``` import { Stage, Text } from 'inlet/react-pixi'; import { RecoilContext, useSetRecoilValue } from 'recoil'; import { theme } from './atoms'; const ContextBridge = ({ children, Context, render }) => { return ( <Context.Consumer> {(value) => render(<Context.Provider value={value}>{children}</Context.Provider>)} </Context.Consumer> ); }; const RendererComponent = () => { const theme = useSetRecoilValue(theme); return <Text text={theme.settings} /> } const App = () => ( <ContextBridge Context={RecoilContext} render={(children) => <Stage>{children}</Stage>} > <RendererComponent /> </ContextBridge> ); ``` Proposed API for Recoil to support context briding without exposing the internals of the Recoil store: ``` function RenderingBridge({children}) { const store = useRecoilStore_UNSTABLE(); return ( <CustomRenderer> <RecoilRoot store_UNSTABLE={store}>{children}</RecoilRoot> </CustomRenderer> ); } function MyApp() { return ( <RecoilRoot> ... <RenderingBridge> ... </RenderingBridge> </RecoilRoot> ); } ``` Fixes facebookexperimental/Recoil#140 Pull Request resolved: facebookexperimental/Recoil#298 Reviewed By: davidmccabe Differential Revision: D22929302 Pulled By: drarmstr fbshipit-source-id: 41c6d8d66d9326da0e1d09583d98934f60ab6ba2
Summary: NOTE: Please note updated context bridging proposal in D22983479 or D22935480 Export RecoilRoot's store via an opaque type to make Context bridging possible. Recoil uses `React.Context` internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem. Here's an example of a Context bridge I use for ReactPixi: ``` import { Stage, Text } from 'inlet/react-pixi'; import { RecoilContext, useSetRecoilValue } from 'recoil'; import { theme } from './atoms'; const ContextBridge = ({ children, Context, render }) => { return ( <Context.Consumer> {(value) => render(<Context.Provider value={value}>{children}</Context.Provider>)} </Context.Consumer> ); }; const RendererComponent = () => { const theme = useSetRecoilValue(theme); return <Text text={theme.settings} /> } const App = () => ( <ContextBridge Context={RecoilContext} render={(children) => <Stage>{children}</Stage>} > <RendererComponent /> </ContextBridge> ); ``` Proposed API for Recoil to support context briding without exposing the internals of the Recoil store: ``` function RenderingBridge({children}) { const store = useRecoilStore_UNSTABLE(); return ( <CustomRenderer> <RecoilRoot store_UNSTABLE={store}>{children}</RecoilRoot> </CustomRenderer> ); } function MyApp() { return ( <RecoilRoot> ... <RenderingBridge> ... </RenderingBridge> </RecoilRoot> ); } ``` Fixes facebookexperimental/Recoil#140 Pull Request resolved: facebookexperimental/Recoil#298 Reviewed By: davidmccabe Differential Revision: D22929302 Pulled By: drarmstr fbshipit-source-id: 41c6d8d66d9326da0e1d09583d98934f60ab6ba2
Export RecoilRoot's
AppContext
to make Context bridging possible.Recoil uses
React.Context
internally, however the context doesn't travel through custom renderers and a Context Bridge is mostly used to solve this problem.To make context bridging possible the AppContext needs to be reachable.
Here's an example of a Context bridge I use for ReactPixi:
Fixes #140