Storybook & Atomic Design – 1.4. – Building Our Application Wrapper

Why would we need a global wrapper?

In our application, we will need a number of high-order, parent components which will pass down global props and information relevant to our application styles, logic and data. You will find this a critical part of your setup if you rely on a ThemeProvider, ApolloProvider or global state store provider.

Fortunately Storybook makes this possible with the availability of a addDecorator function which is available to React Storybook environments.

By using this function within our config.js file, we can add a global component to all of our stories.

Global Storybook Wrapper

To get started we'll first import React, followed by the addDecorator and configure function from Storybook

We don't have to make any changes to our import configuration for stories, so we can skip over that.

Next, we'll compose a GlobalWrapper React component to add to all of our stories, you can name it what you want but to be descriptive I've used 'GlobalWrapper' as the name for the functional component.

When creating a decorator for our stories, we have one prop available to us named storyFn. This prop is a function which returns the storybook component being viewed.

So if we were looking at our newly created Button component in Storybook within the browser, storyFn would return the Button component to our render.

To create a basic Storybook Wrapper and get visual feedback that it's working, I've wrapped all components in our system within a div.

With an H2 preceding our Component with the text 'Hello world'.

Original, I know.

import React from "react";
import { addDecorator, configure } from "@storybook/react";

// automatically import all files ending in *.stories.js
configure(require.context("../components", true, /\.stories\.js$/), module);

const GlobalWrapper = storyFn => (
    <div>
        <h2>Hello world</h2>
        {storyFn()}
    </div>
);

addDecorator(GlobalWrapper);

If you've already got Storybook running, you will need to kill the terminal process and run the start command again. This is because we've updated the configuration for Storybook itself and it will need to be restarted to detect the new configuration.

npm run storybook

You should now have Storybook available at http://localhost:6006/

Now when we visit our Button component, we should get a heading level 1 element with the text 'Global', followed by our component.

Screenshot of a basic button component in Storybook, with a H2 preceding the button including the text 'Hello world'

This is great because any future components we create will be wrapped in this Global Wrapper component we've composed allowing us to update multiple story contexts without manually going through each component or story.

Theme Provider

So we've got our global component wrapper, but I'm guessing you wouldn't want to insert a Heading level 2 on every story with the text 'Hello world', so we'll remove that element now.

const GlobalWrapper = storyFn => (
    <>
        {storyFn()}
    </>
);

addDecorator(GlobalWrapper);

Next, we want to introduce a package called styled-components.

Styled Components (CSS-in-JS)

Styled Components is a CSS-in-JS package which will allow us to write CSS that is isolated to our components and makes use of props to render smart classes that are reactive to data.

We'll be digging more into how to write the CSS for our components in a following lesson, so don't worry if you've never used the package before.

The reason we're looking at the package now is that traditionally those who work on front-end development would be used to using a _variables.scss file in their setups, or some method of handling reusable values associated with a brands colour palette, font selection, or any other visual values.

However, in our setup, we aren't using SCSS/SASS/LESS or any pre-processing languages, but we still want to have access to some predefined values that we can reference when styling say a buttons background colour so that it uses colours aligned with the brand's predefined palette.

When using Styled Components we achieve this with the ThemeProvider component and by passing in our ThemeDefaultobject to this component as a themeprop.

The theme object is will hold keys such as primary or secondary which will hold hexadecimal colour values for the brand which we want to make available to our CSS-in-JS files later on. We will also include font names, shades of grey, spacing values and more.

By wrapping our application in a ThemeProvider component with the prop theme assigned to the value of our theme object, we can reference these values almost anywhere within our application when accessing the prop values for a component.

Installing Styled Components

Before we start writing any code, let's first install the package styled-components using -

npm i styled-components

We'll start off by importing the ThemeProvider component from styled-components and replace our div with the ThemeProvider component.

Next, we'll create the theme object with one key named 'primary' with the value '#00ffff'.

Finally, we pass the theme object into the ThemeProvider component and save the file.

import React from "react";
import { addDecorator, configure } from "@storybook/react";
import { ThemeProvider } from "styled-components";

// automatically import all files ending in *.stories.js
configure(require.context("../src/components", true, /\.stories\.js$/), module);

const ThemeDefault = {
    primary: "#5D3DDC"
};

const GlobalWrapper = storyFn => (
    <ThemeProvider theme={ThemeDefault}>{storyFn()}</ThemeProvider>
);

addDecorator(GlobalWrapper);

We won't see the benefits of this immediately as we haven't written any CSS using styled-components yet but it sets us up to make use of the theme later on.

GlobalStyle

Finally, we'll introduce a GlobalStyle component.

The GlobalStyle component is responsible for applying global CSS rules that we want to cascade throughout our application.

These rules would include default rules for common HTML elements such as

  • Anchor links
  • Default button styles (for where we don't reference our component)
  • Forms
  • Font imports and applying the font rules to the body of the page
  • Heading elements
  • Inputs
  • Lists

Any CSS rules that live in the GlobalStyle will be applied first before our component styles and serve as a fallback styled default. It's important that we include this outside of Styled-Components to make the best use of the cascade available to us.

import React from "react";
import { addDecorator, configure } from "@storybook/react";
import { createGlobalStyle, ThemeProvider } from "styled-components";

// automatically import all files ending in *.stories.js
configure(require.context("../components", true, /\.stories\.js$/), module);

const ThemeDefault = {
    primary: "#4E2ECD"
};

const GlobalStyle = createGlobalStyle`
    body {
        background-color: ${props => props.theme.primary};
        color: black;
    }
`;

const GlobalWrapper = storyFn => (
    <ThemeProvider theme={ThemeDefault}>
        <GlobalStyle />
        {storyFn()}
    </ThemeProvider>
);

addDecorator(GlobalWrapper);
Screenshot of a basic button component in Storybook, with a rebeccapurple background colour applied to the body of the story

Reworking Our Component Structure

As our ThemeDefault object isn't necessarily isolated to being used in only in our ThemeProvider component, I want to separate it out to it's own file. That way, if for any reason want to import these values elsewhere we can do so.

I also don't like the idea of storing our GlobalStyle rules in our Storybook wrapper. Not only will it result in a very large file as we later add our default element styles, but these styles are to be used in other templates, such as a global page template in a Gatsby or Next environment.

To amend these issues, I've created two new components in our components directory under the concept of particles.

- __Storybook__
   - __components__
     - __particles__
       - globalStyles.jsx
       - themeDefault.jsx

globalStyle.jsx

import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
    body {
    	background-color: ${props => props.theme.primary};
        color: black;
    }
`;

export default GlobalStyle

themeDefault.jsx

const ThemeDefault = {
    /* Colours */
    primary: "#4E2ECD",
}

module.exports = ThemeDefault

Final Global Wrapper Markup

import React from "react";
import { addDecorator, configure } from "@storybook/react";
import { ThemeProvider } from "styled-components";

import GlobalStyle from "../src/components/particles/globalStyle";
import ThemeDefault from "../src/components/particles/themeDefault";

// automatically import all files ending in *.stories.js
configure(require.context("../components", true, /\.stories\.js$/), module);

const GlobalWrapper = storyFn => (
    <ThemeProvider theme={ThemeDefault}>
        <GlobalStyle />
        {storyFn()}
    </ThemeProvider>
);

addDecorator(GlobalWrapper);

Now we have access to a global stylesheet that is applied to all of our stories, as well as access to our theme properties when writing future CSS-in-JS for components.

Before the next lesson, I'll be adding some default element styles, and resetting the browsers CSS with normalize.css. The reason I'm not covering it in this episode is to save us some time but if you want to follow along, you can look at the theme properties I've set on Github, available at the commit -

In the next lesson, we'll be covering how we can build a custom React button component that dynamically returns an HTML element to the DOM based on the props we pass it.

We'll also add several stories to show the different variations of this React button and how it can be used in a front-end project.

Continue Reading 📚