I have an application where users will click various parts of the application and this will display some kind of configuration options in a drawer to the right.
The solution I've got for this is to have whatever content that is to displayed, to be stored in context. That way the drawer just needs to retrieve its content from context, and whatever parts of that need to set the content, can set it directly via context.
Here's a CodeSandbox demonstrating this.
Key code snippets:
const MainContent = () => {
const items = ["foo", "bar", "biz"];
const { setContent } = useContext(DrawerContentContext);
/**
* Note that in the real world, these components could exist at any level of nesting
*/
return (
<Fragment>
{items.map((v, i) => (
<Button
key={i}
onClick={() => {
setContent(<div>{v}</div>);
}}
>
{v}
</Button>
))}
</Fragment>
);
};
const MyDrawer = () => {
const classes = useStyles();
const { content } = useContext(DrawerContentContext);
return (
<Drawer
anchor="right"
open={true}
variant="persistent"
classes={{ paper: classes.drawer }}
>
draw content
<hr />
{content ? content : "empty"}
</Drawer>
);
};
export default function SimplePopover() {
const [drawContent, setDrawerContent] = React.useState(null);
return (
<div>
<DrawerContentContext.Provider
value={{
content: drawContent,
setContent: setDrawerContent
}}
>
<MainContent />
<MyDrawer />
</DrawerContentContext.Provider>
</div>
);
}
My question is - is this an appropriate use of context, or is this kind of solution likely to encounter issues around rendering/virtual dom etc?
Is there a tidier way to do this? (ie. custom hooks? though - remember that some of the components wanting to do the setttings may not be functional components).
Note that it is fine to store components in Context purely on the technical point of it since JSX structures are nothing but Objects finally compiled using React.createElement
.
However what you are trying to achieve can easily be done through portal and will give you more control on the components being rendered elsewhere post you render it through context as you can control the state and handlers for it better if you directly render them instead of store them as values in context
One more drawback of having components stored in context and rendering them is that it makes debugging of components very difficult. More often than not you will find it difficult to spot who the supplier of props is if the component gets complicated
import React, {
Fragment,
useContext,
useState,
useRef,
useEffect
} from "react";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import { Drawer } from "@material-ui/core";
import ReactDOM from "react-dom";
const useStyles = makeStyles(theme => ({
typography: {
padding: theme.spacing(2)
},
drawer: {
width: 200
}
}));
const DrawerContentContext = React.createContext({
ref: null,
setRef: () => {}
});
const MainContent = () => {
const items = ["foo", "bar", "biz"];
const [renderValue, setRenderValue] = useState("");
const { ref } = useContext(DrawerContentContext);
return (
<Fragment>
{items.map((v, i) => (
<Button
key={i}
onClick={() => {
setRenderValue(v);
}}
>
{v}
</Button>
))}
{renderValue
? ReactDOM.createPortal(<div>{renderValue}</div>, ref.current)
: null}
</Fragment>
);
};
const MyDrawer = () => {
const classes = useStyles();
const contentRef = useRef(null);
const { setRef } = useContext(DrawerContentContext);
useEffect(() => {
setRef(contentRef);
}, []);
return (
<Drawer
anchor="right"
open={true}
variant="persistent"
classes={{ paper: classes.drawer }}
>
draw content
<hr />
<div ref={contentRef} />
</Drawer>
);
};
export default function SimplePopover() {
const [ref, setRef] = React.useState(null);
return (
<div>
<DrawerContentContext.Provider
value={{
ref,
setRef
}}
>
<MainContent />
<MyDrawer />
</DrawerContentContext.Provider>
</div>
);
}
CodeSandbox demo
Regarding performance, components subscribed to a context will rerender if the context changes, regardless of whether they store arbitrary data, jsx elements, or React components. There is an open RFC for context selectors that solves this problem, but in the meantime, some workarounds are useContextSelector and Redux.
Aside from performance, whether it is a misuse depends on whether it makes the code easier to work with. Just remember that React elements are just objects. The docs say:
React elements are plain objects and are cheap to create
And jsx is just syntax. The docs:
Each JSX element is just syntactic sugar for calling React.createElement(component, props, ...children).
So, if storing { children: 'foo', element: 'div' }
is fine, then in many cases so is <div>foo</div>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With