Include Context in ReactExample and Wrapper
#1,639 opened on Jul 20, 2020
Repository metrics
- Stars
- (11,091 stars)
- PR merge metrics
- (PR metrics pending)
Description
I'm not sure if this is a feature request 🚀, bug report 🐛, or support question 🤔! I think I figured out what's going on, what (IMHO) should be happening, and a workaround, but if there's already a way to accomplish this then please let me know.
The problem
I would like to be able to access Context from my custom Wrapper, but when ReactExample is rendered, it loses context. As is noted in the docs:
Each example is rendered in an independent React root
You can see this is the screenshot, which includes the React Component dev tools layout, showing that ReactExample is rendered outside of Context.Provider.
The docs go on to say in the next sentence:
You can control React Context by defining a custom Wrapper component like this:
// styleguide.config.js const path = require('path') module.exports = { styleguideComponents: { Wrapper: path.join(__dirname, 'src/styleguide/Wrapper') } }
So the docs make it sound like you have access to the React Context in a Wrapper component, but I didn't find this to be the case. I attempted a themed wrapper using rsg-components/Context, but the context that was in Wrapper was reset to initial state. I'll provide my relevant code here, then attempt to reproduce a minimal repo later.
// styleguide.config.js
module.exports = {
...,
styleguideComponents: {
StyleGuideRenderer: path.join(__dirname, 'styleguide/StyleguideWrapper'),
// Wraps each example component with Provider to apply branded styled-components/theme
Wrapper: path.join(__dirname, 'styleguide/ThemedWrapper.jsx'),
},
}
/**
* StyleGuideWrapper.jsx
* Essentially copies StyleGuideRenderer.jsx, prior to conversion to TypeScript (styles and less
* relevant pieces omitted here!)
*/
import React from 'react';
import cx from 'clsx';
import Context, { useStyleGuideContext } from 'rsg-components/Context';
import Styled from 'rsg-components/Styled';
import ThemeSwitcher from './ThemeSwitcher';
export function StyleGuideRenderer({ children, classes, hasSidebar, toc }) {
/**
* Copy of the Context set with Context.Provider in parent, StyleGuide.js, which are set in a new
* Context.Provider here so I can add custom `brand` to Context and only overwrite the renderer
* and not the main logic:
*/
const {
codeRevision,
config,
slots,
displayMode,
cssRevision,
} = useStyleGuideContext();
const [brand, setBrand] = useState('brand1');
return (
<Context.Provider value={{
brand,
setBrand,
codeRevision,
config,
slots,
displayMode,
cssRevision,
}}>
<div className={cx(classes.root, hasSidebar && classes.hasSidebar)}>
<main className={classes.content}>
{children}
</main>
{hasSidebar && (
<div className={classes.sidebar} data-testid="sidebar">
<section className={classes.sidebarSection}>
<ThemeSwitcher classes={classes} />
</section>
{toc}
</div>
)}
</div>
</Context.Provider>
);
}
StyleGuideRenderer.propTypes = propTypes;
export default Styled(styles)(StyleGuideRenderer);
import React from 'react';
import Styled from 'rsg-components/Styled';
import { useStyleGuideContext } from 'rsg-components/Context';
const ThemeSwitcher = ({ classes }) => {
const { brand, setBrand } = useStyleGuideContext();
const onBrandChange = (e) => setBrand(e.target.value);
const brands = ['brand1', 'brand2'];
return (
<label className={classes.root}>
Brand
<select
value={brand}
onBlur={onBrandChange}
onChange={onBrandChange}
>
{brands.map((b) => (
<option key={b} value={b}>{b}</option>
))}
</select>
</label>
);
};
export default Styled(styles)(ThemeSwitcher);
// ThemedWrapper.jsx
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { useStyleGuideContext } from 'rsg-components/Context';
function ThemedWrapper({ children }) {
const { brand } = useStyleGuideContext();
return (
<ThemeProvider theme={brand}>
{children}
</ThemeProvider>
);
}
export default ThemedWrapper;
The key part--what isn't working but should-- is in ThemedWrapper.jsx:
const { brand } = useStyleGuideContext();
If I log the output of useStyleGuideContext() in ThemedWrapper.jsx, it shows the default Context, not the updated Context that logs when running the same method from StyleGuideWrapper.jsx.
Hacky solution
I'm calling this hacky, because it's what I got working, but requires overriding an additional rsg component. Each ReactExample component is rendered in Preview, and this is where it loses context. If those are wrapped with <Context.Provider>, then it works! But in order to do this, I had to copy-paste the most recent JS version of rsg components/Preview into a new PreviewWrapper.jsx and replace Preview with that in the config. This feels brittle to me and I want to make as few replacements as possible so my library doesn't get too far from the styleguidist source code and is able to take future updates.
Code changes to make it work:
// PreviewWrapper.jsx
// This is a copy of rsg-components/Preview.js, with the following changes:
// Also update with changes to context
componentDidUpdate(prevProps, prevContext) {
if (this.props.code !== prevProps.code || this.context.brand !== prevContext.brand) {
this.executeCode();
}
}
// Wrap <ReactExample /> inside <Context.Provider>
executeCode() {
...
const wrappedComponent = (
<Context.Provider value={this.context}>
<ReactExample
code={code}
evalInContext={this.props.evalInContext}
onError={this.handleError}
compilerConfig={this.context.config.compilerConfig}
/>
</Context.Provider>
);
...
}
Proposed solution
Based on the docs stating "You can control React Context by defining a custom Wrapper component like this...", it seems to me like this is a bug and is missing expected behavior. In that case, my proposed solution is to fix it by updating rsg-components/Preview to pass update-to-date Context to ReactExample, which would allow library users to access Context in a custom Wrapper.
Alternative solutions
If this isn't a bug and the proposed fix doesn't seem reasonable, then my alternative solution is to at least update rsg-components/Preview to have a PreviewRenderer so that library users can update that custom component and overwrite less core functionality.
Additional context
I opened a StackOverflow question about this on Friday, although it's a bit behind this, as I discovered rsg-components/Context since then and made more progress in my understanding of the issue.
I think I would be able to do either proposed solution and submit a pr, but would like feedback if there's anything I'm missing here about expected behavior. I'd be happy to learn that I simply wasted a week of code trekking and that it's already easy to make this work :sweat_smile:. In that case, I'll gladly update the docs! :grin: Thank you!