A few of my recent articles have been embedding limited builds of Glorious Voice Leader directly into the page. At first, this presented an interesting challenge. How could I render a single React application across multiple container nodes, while maintaining shared state between all of them?
While the solution I came up with probably isn’t best practice, it works!
As a quick example, imagine you have a simple React component that manages a single piece of state. The user can change that state by pressing one of two buttons:
const App = () => {
let [value, setValue] = useState("foo");
return (
<div>
<button onClick={() => setValue("foo")}>
Value is "{value}". Click to change to "foo"!
</button>
<button onClick={() => setValue("bar")}>
Value is "{value}". Click to change to "bar"!
</button>
</div>
);
};
Normally, we’d render our App
component into a container in the DOM using ReactDOM.render
:
ReactDOM.render(<App />, document.getElementById('root'));
But what if we want to render our buttons in two different div
elements, spread across the page? Obviously, we could build out two different components, one for each button, and render these components in two different DOM containers:
const Foo = () => {
let [value, setValue] = useState("foo");
return (
<button onClick={() => setValue("foo")}>
Value is "{value}". Click to change to "foo"!
</button>
);
};
const Bar = () => {
let [value, setValue] = useState("foo");
return (
<button onClick={() => setValue("bar")}>
Value is "{value}". Click to change to "bar"!
</button>
);
};
ReactDOM.render(<Foo />, document.getElementById('foo'));
ReactDOM.render(<Bar />, document.getElementById('bar'));
But this solution has a problem. Our Foo
and Bar
components maintain their own versions of value
, so a change in one component won’t affect the other.
Amazingly, it turns out that we can create an App
component which maintains our shared state, render that component into our #root
container, and within App
we can make additional calls to ReactDOM.render
to render our Foo
and Bar
components. When we call ReactDOM.render
we can pass down our state value and setters for later use in Foo
and Bar
:
const App = () => {
let [value, setValue] = useState("foo");
return (
<>
{ReactDOM.render(
<Foo value={value} setValue={setValue} />,
document.getElementById("foo")
)}
{ReactDOM.render(
<Bar value={value} setValue={setValue} />,
document.getElementById("bar")
)}
</>
);
};
Our Foo
and Bar
components can now use the value
and setValue
props provided to them instead of maintaining their own isolated state:
const Foo = ({ value, setValue }) => {
return (
<button onClick={() => setValue("foo")}>
Value is "{value}". Click to change to "foo"!
</button>
);
};
const Bar = ({ value, setValue }) => {
return (
<button onClick={() => setValue("bar")}>
Value is "{value}". Click to change to "bar"!
</button>
);
};
And everything works! Our App
is “rendered” to our #root
DOM element, though nothing actually appears there, and our Foo
and Bar
components are rendered into #foo
and #bar
respectively.
Honestly, I’m amazed this works at all. I can’t imagine this is an intended use case of React, but the fact that it’s still a possibility made my life much easier.
Happy hacking.