React-styleguidist: Include Context in ReactExample and Wrapper

Created on 21 Jul 2020  ยท  9Comments  ยท  Source: styleguidist/react-styleguidist

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.
Screen Shot 2020-07-20 at 3 44 04 PM

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!

enhancement help wanted

All 9 comments

Here is what appears to be a _non-working_ replica: https://github.com/apennell/example. I based it on the repo that styleguidist says to use to replicate bugs, but it turned out to be rather old. I tried to get it up to date, with the latest react-styleguidist and the same config as I've been using, but it hits a babel config issue. I replaced babel-core with @babel/core ^7, I get:

 FAIL  Failed to compile

./styleguide/StyleguideWrapper.jsx
Error: Cannot find module 'babel-core'
 @ ./node_modules/react-styleguidist/lib/client/rsg-components/StyleGuide/StyleGuide.js 24:0-78 121:40-58
 @ ./node_modules/react-styleguidist/lib/client/rsg-components/StyleGuide/index.js
 @ ./node_modules/react-styleguidist/lib/client/utils/renderStyleguide.js
 @ ./node_modules/react-styleguidist/lib/client/index.js
 @ multi ./node_modules/react-styleguidist/lib/client/index ./node_modules/react-dev-utils/webpackHotDevClient.js
./node_modules/react-styleguidist/lib/client/rsg-components/Playground/Playground.js
Module not found: Can't resolve 'rsg-components/Preview' in '/Users/annie/Code/styleguidist-example/example/node_modules/react-styleguidist/lib/client/rsg-components/Playground'

I removed my node modules and reinstalled, and it doesn't say in the package-lock.json that styleguidist is requesting babel-core, so not sure why this is tripping here.

If it would be useful to see a working replica of what I'm talking about, let me know and I can try to spin up a new copy.

So the docs make it sound like you have access to the React Context in a Wrapper component,

Nope, they say you can define context in the Wrapper that will be accessible in your component. We can definitely make this more clear and add some examples โ€” feel free to send a pull request ;-)

Sorry, I haven't read the whole novel, as I understand you want to access the Styleguidist context from the Wrapper component? I think would be a useful feature.

Sorry for the long issue, wanted to be sure to include the details! Also probably helped me rubber duck my problem/solution a bit ๐Ÿ˜

I see what you mean about defining context in the Wrapper and having access to _that_, as opposed to the higher up Styleguidist context that I was hoping for. I'll try to think of a way that this might be stated more explicitly and make a pr if I can.

The tldr; of my novel is that I want access to the same context in each example, which I'm saving in the the Styleguidist context through a custom StyleguideRenderer. When Context.brand changes, each example should be notified of the change so the theme can update. I was able to achieve this with an update to Preview.js, but am hoping that I can make this a change available to the library instead of my own override.

Does my proposal sound worth pursuing, or is there a reason that this wouldn't be good to add in universally?

Proposed solution
Update rsg-components/Preview to pass update-to-date Context to ReactExample and check for changes to this.context in componentDidUpdate, which would allow library users to access Context in a custom Wrapper.

This would make these updates to Preview.js:

// Also check for changes to `this.context` and run `executeCode()` if changes
componentDidUpdate(prevProps, prevContext) {
  if (this.props.code !== prevProps.code || this.context !== prevContext) {
    this.executeCode();
  }
}

// Add <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>
  );
  ...

If you think this would be a good update, then I'll see if I can figure out Typescript and give it a shot on Friday.

I'm thinking now that this might be not the best solution, because this way you'll have access to Styleguidist internal context, and it would make sense to isolate it from whatever you may want to put there.

https://github.com/styleguidist/react-styleguidist/blob/9b6b6f60051f170dcb7f548fb114420a48a8e1e9/src/client/rsg-components/Context/Context.ts#L4-L10

So maybe we should have a separate context for the user?

After resolving this with my own work-around, I'm back and looking at how I might be able to provide this feature for the library. However, I'm not sure how to extend my solution for general use. I made a PreviewWrapper component that basically copied Preview and checks for an update to my specific context string (context.brand) in componentDidUpdate, otherwise executeCode isn't run and the context change doesn't trigger an update.

What I ultimately needed to achieve was the ability to change themes through a select menu in the sidebar. Would it be useful/welcome if I skipped the idea of adding access to general context and instead submitted a PR with access to this theming functionality? The config could include an option to pass in a themes array, and if present would add a "Themes" select menu to the sidebar that would control context.theme and provide that context to each rendered example.

Here's the general concept, with the "Brand" menu: https://coverhound.github.io/covered-ui/

I think this is a great idea! ๐Ÿฆ„

Great! Hopefully I can find time to quickly learn TypeScript and add this in ๐Ÿ˜

+1 this - was exactly what I was trying to work out how to do

The config could include an option to pass in a themes array, and if present would add a "Themes" select menu to the sidebar that would control context.theme and provide that context to each rendered example.

@apennell This is basically identical to the hack I was thinking about when I found this issue. This also feels like a well defined scope. ๐Ÿ‘ looking forward to seeing your code - if you want any help I'm happy to lend a hand, I just pulled down the rsg repo. Theres lots there but it feels easy to navigate and work things out.

Was this page helpful?
0 / 5 - 0 ratings