-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Nesting Fixture #19531
Nesting Fixture #19531
Conversation
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 6b95f87:
|
@@ -0,0 +1 @@ | |||
src/*/node_modules |
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 is awkward. I wish it just worked in CRA, but it doesn't.
|
||
# copies of shared | ||
src/*/shared | ||
src/*/node_modules |
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 is the new part compared to vanilla CRA.
import ThemeContext from './shared/ThemeContext'; | ||
import withLegacyRoot from './withLegacyRoot'; | ||
|
||
const Greeting = withLegacyRoot(() => import('../legacy/Greeting')); |
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 is the API.
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.
(If you build in prod mode, webpack puts the second React in a chunk, as expected.)
} | ||
); | ||
} | ||
throw record.promise; |
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.. This is Suspense. I wanted to use lazy
but that doesn't work because the component is going to be rendered by the other React but we want this React to wait. And we can't suspend in the commit phase.
I don't know if this is bad. It seems like it's part of the contract anyway and there are fairly major libs relying on it. Including Loadable Components. So we might as well use it in an example?
The benefit is that I think this pretty strongly encourages you to code-split the other React.
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.
We could also tuck it into a library but seems nice to have a full view of the setup for customization.
import {useState, useEffect} from 'react'; | ||
|
||
export default function useTimer() { | ||
const [value, setValue] = useState(() => new Date()); |
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 is just to show that Hooks work in "shared" code.
|
||
const theme = useContext(ThemeContext); | ||
const router = useContext(__RouterContext); | ||
const context = useMemo( |
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.
Context collected to be bridged.
); | ||
} | ||
|
||
export default function createLegacyRoot(container) { |
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 intentionally mirrors createRoot
so it's easier to transition a file like this to it when it exists — for people bridging in the reverse direction.
Nice! This mostly even works with DevTools. (The "inspect" button doesn't work for the inner React but that's probably fine. |
Just a quick note here (already spoke with @gaearon about this on Twitter DM): This won't work with React Router v6+ because we don't expose context at all in the |
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.
LGTM, this is a nice way to brute force it to work generically.
There's gotta be a way to do this without copying the shared files so you can hook it up in place, but that would probably require a deeper integration into the build system or environment.
Also, I added a README 🚀
Where do I put a Flux store that is used by both legacy and modern? It's only ok if it doesn't any have dependencies on React. |
You'd put it above. (But yes, that presumes no dependency on React.) Why would a Flux store depend on React? |
Maybe we should add a Flux store to demonstrate that? Seems non-obvious since it seems like you could put it in shared but that won't work. |
Does Redux count as Flux? :-) |
Yes. A wolf in sheep's clothing is still a wolf. |
It might be good to try this with a build of React that renames all the internal fields and symbols to make sure it actually works with a different implementation. |
Co-authored-by: Ricky <rickhanlonii@gmail.com>
Yeah I messed it up. Should be fixed. |
35c4791
to
aaac26f
Compare
Added Redux to the example. Tested with a version that has different Symbols and internal Fiber field names. |
This is a fixture that demonstrates a build and runtime setup for using two copies of React on the same page.
Note: this is usually a really bad idea. But in large legacy apps, sometimes the only alternative is to stop upgrading React altogether. This fixture is meant to demonstrate an alternative solution — partial (e.g. per-route) upgrades/holdouts.
Hooks are working: People often struggle setting this up (Hooks + multiple instances of React #13991). In particular, the constraint is that the version of
react
seen by a component needs to be the same version ofreact
that is seen byreact-dom
that rendered this component.No bundler config tweaks: This is intentionally using an unejected CRA. I wanted to show an approach that should work regardless of your bundler (as long as it respects the Node resolution mechanism). Also works with both Yarn and npm.
Context bridge: This demo includes passing context through a cross-React bridge. This is vital in real apps. Without it, you can't use anything — even a router — in practice. This example includes React Router to show that. (It relies on a private API, but I'll add a scary comment, and I got an okay from @mjackson for this particular use case).
Code splitting: You pretty much never want to load two Reacts if it's avoidable. So ideally it would only be used for lazy-loaded code, e.g. a legacy popup dialog or a legacy route. To encourage this, I made a
lazyLegacyRoot(() => import(...))
wrapper that is similar in spirit tolazy()
. You use<Suspense>
to show the loading state.Code sharing: When you split code between legacy and modern folders, there will be a bunch of React code you want to share. But since we rely on Node resolution, we can't have the same files having different meaning of the
react
import depending on whether they were imported fromsrc/modern
orsrc/legacy
. As a workaround, I addedsrc/shared
folder that is being watched. Every change in it gets replicated by a watcher intosrc/modern/shared
andsrc/legacy/shared
which are ignored in source control. This is pretty low-tech and the part I like about this setup the least. But it seems like that's also the part you can replace on your own if you're ready to fiddle with the configs. Note that this does mean this code gets duplicated between the two bundles. I don't have a good solution to that.Third-party libraries: We have three
package.json
: one at the root for build tooling, one insrc/modern/package.json
and one insrc/legacy/package.json
. Reacts are defined in the nested ones. This is where you put third-party libraries that depend on React. The nestedpackage.json
s may use different libraries or even different versions. Note thatsrc/shared
has nopackage.json
, but files there can use third-party libs (else we wouldn't be able to share Hooks and Components). Again, this works becausesrc/shared/*
gets copied intoshared
subdirectories of bothmodern
andlegacy
.As an exercise and to verify this setup works, I converted a simple (but not trivial) app to this setup locally, extracting one of the component subtrees into
legacy
. The most frustrating part of this process was figuring out which code needs to beshared
and which parts are not, and then changing a bunch of relative paths. The rest went fairly smoothly and the end result worked as soon as I bridged all contexts. So overall I think this works but maybe it could be more ergonomic. However, I still think requiring fiddling with config would make this example a lot less versatile and useful.This repo in particular demonstrates a
legacy
app nested into amodern
one. But we could swap folders and do the opposite. There's nothing in particular that enforces the direction.Note: In React repo I notice an issue where
npm start
shows a bunch of ESLint warnings, but a secondnpm start
works. I couldn't reproduce it outside of our repo so this seems like a CRA quirk that's unrelated to this demo.