State management in React is a widely discussed topic on the internet. While the most common approach for large applications seems to be Redux, there has been a lot of attention on the improvements of the React Context API. Using render props, we can now access state being held far up the tree with ease. For a single page application, this is ideal because the whole application is one tree.
However, if you are mounting several trees of components throughout the page (in my case I'm using Ruby on Rails and Webpacker React to render the components), the different trees cannot use the same provider as a common ancestor. Hence, they cannot share the same state. So how do we get the different trees to play nicely together?
React Context + React Portals
Using a syntax very similar to:
ReactDOM.render(<MyComponent />, domElement);
We can use
const PortalComponent = React.createPortal(<MyComponent />, domElement);
to render multiple components in the same React tree, among different isolated parts of the DOM. Using React.createPortal()
we can create a new React component to be rendered at the specified DOM node.
What's different about createPortal()
when compared to ReactDOM.render()
, is that the component is not rendered at the point of calling createPortal()
. We use the return value of React.createPortal()
(a new React component), and render it in our tree. Here's an example where we are rendering components with different entry points throughout the page:
/* first let's define a context to share */
/* important_value_context.jsx */
import React from 'react'
const Context = React.createContext({})
export class ImportantValueProvider extends React.PureComponent {
state = {
importantValue: 'Cake is good',
updateImportantValue: this.updateImportantValue,
}
updateImportantValue = importantValue => this.setState({importantValue})
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
export default Context
/* main.jsx */
import React from 'react'
import ReactDOM from 'react-dom'
import {ImportantValueProvider} from 'path/to/important_value_context'
import CoolComponent from 'path/to/cool_component'
import SuperCoolComponent from 'path/to/super_cool_component'
const Main = () => {
const ComponentA = () => ReactDOM.createPortal(
<CoolComponent />,
document.getElementById('banner'),
)
const ComponentB = () => ReactDOM.createPortal(
<SuperCoolComponent />,
document.getElementById('footer'),
)
return (
<ImportantValueProvider>
<ComponentA />
<ComponentB />
</ImportantValueProvider>
)
}
ReactDOM.render(
<Main />,
document.getElementById('main'),
)
Instead of making two calls to ReactDOM.render()
, we create two portals and render both under our top-level Provider. ComponentA
and ComponentB
will be rendered in two different points in the DOM, but they share the same React tree, thanks to portals. They can both import ImportantValueContext
and reference its consumer to communicate with the same instance of ImportantValueContext
.
/* cool_component.jsx */
import React from 'react'
import ImportantValueContext from 'path/to/important_value_context'
class CoolComponent extends React.PureComponent {
render() {
return (
<ImportantValueContext.Consumer>
{sharedState => (
<div>
<input
value={sharedState.importantValue}
onChange={e => sharedState.updateImportantValue(e.target.value)}
/>
</div>
)}
</ImportantValueContext.Consumer>
)
}
}
export default CoolComponent
/* super_cool_component.jsx */
import React from 'react'
import ImportantValueContext from 'path/to/important_value_context'
class SuperCoolComponent extends React.PureComponent {
render() {
return (
<ImportantValueContext.Consumer>
{sharedState => (
<h1>{sharedState.importantValue}</h1>
)}
</ImportantValueContext.Consumer>
)
}
}
export default SuperCoolComponent
Now CoolComponent
and SuperCoolComponent
can share state with the same ImportantValueProvider. That's fantastic news if you plan on sprinkling components into an existing webpage... if you're into that sort of thing.
NOTE: This applies to working with Redux providers as well, though using context was my use case so I put emphasis on that instead.
Top comments (4)
What about?
In this case they are in different trees and cant share context or can they?
Libraries that export a
Modal
component are usually using aPortal
under the hood. In this case, you do need that<Modal>
inside of the<ImportantValueProvider>
. There's no way around it.Your
Provider
should be at the top of your application, unless you have multiple instances of the same kind ofProvider
, which is sort of an anti-pattern unless you have a niche use case that fits it. In short, put yourProvider
at the top-level of your app and you should be good to go.Hi Michael, thanks for share, I'm doing an idea and your tutorial helped me a lot.
But implementing I had one problem, I don't know if it was because of the version of react or react-dom, to fix, I needed to change one piece of the code in file main.jsx
From:
For:
For some reason, when I use constants instead of functions in my setup (react and react-dom: 16.8.6) , react throw this error:
If someone else get the same error/problem, the solution above may will work =]
Cheers o/
You're right, never trust the internet!
Thanks for the kind words!
Edit: Added the functions so it works now