Skip to main content

Gatsby with Redux using a Gatsby theme

Published on

Even though Gatsby is a static site generator, it can be used to build dynamic applications. Gatsby is, after all, just a framework for building a React application. It just happens to be extremely opinionated and generates an artifact that is a static site. To me, "dynamic" means that an application maintains some form of state. I perform an action and the application "remembers" and allows me to perform more interactions based on this state. A common method for managing state in JavaScript applications is Redux and combined with React Redux to provide React framework bindings to the Redux library.

You don't always need Redux for state management, but it sure is useful as you begin building more complex applications. For example, an eCommerce frontend. Redux makes it easy to say "hey, I added this to the cart", do some API calls, handle the respone, and store information about the response in the state. React can then rebuild its component tree based on these changes. And poof! The application shows information about your cart with its new item. Magic 🥳.

I have added Redux to a Gatsby application before. But what about a theme? Can we add Redux to a Gatsby theme that Gatsby application then inherit? We should. Should we? I don't know, but I will! Unfortunately, the documentation on using Redux as a data storage in Gatsby has a stub, but no content. There is a using-redux example in the Gatsby development repository, though.

Can this even work with a theme?

First off: I didn't even know if this would work. You need to hook into the server-side rendering and browser hooks to create your Redux state and wrap the application mount with the React Redux bindings. This involves overwriting the wrapRootElement function in both gatsby-ssr.js and gatsby-browser.js. This function allows a plugin to wrap the element the application mounts.

Here's an example of what you would provide for wrapRootElement, taken from the Gatsby development repository example

import React from "react"
import { Provider } from "react-redux"

import createStore from "./src/state/createStore"

export default ({ element }) => {
  const store = createStore()
  return <Provider store={store}>{element}</Provider>
}

It receives the root element and wraps it in a provider, which is binding for Redux. Great! I have implemented this in a Gatsby site before, directly. The documentation specifies it allows a plugin to wrap the root element. Themes are basically plugins, but with more scope. 🤔so, let's try.

It works! I put everything together and it didn't explode. I added some Redux actions and reducers and boom, it just worked.

Shadowing when using Redux

The React Redux library promotes using presentational components and container components. Presentational components receive props and do not maintain state. The container components subscribe to your state and pass props from the state into the presentational components. Now, in the little bit that I have dabbled with React and Redux, I never did this. I was lazy. However. Component shadowing in Gatsby works really well if you actually follow their suggestions.

Take a product display page, for example. A product display page contains one or more variations (a medium blue t-shirt, a large blue t-shirt, etc.) Based on the current variation, different data renders. The currently selected variation resides in the application state. Here is a product container, which passes props to a presentation component for the product display page.

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { setDefaultVariation } from '../redux/actions/productDisplay'

import ProductDisplay from './productDisplay';

class ProductDisplayContainer extends PureComponent {
    componentDidMount() {
        const { pageContext: { element }, setDefaultVariation } = this.props;
        setDefaultVariation(element.relationships.variations[0]);
    }
    render() {
        const { pageContext: { element, productType }, selectedVariation } = this.props;
        return <ProductDisplay product={element} productType={productType} selectedVariation={selectedVariation} />
    };
}
function mapStateToProps(state) {({
    selectedVariation: state.productDisplay.selectedVariation,
})}
function mapDispatchToProps(dispatch) {({
    setDefaultVariation: variation => dispatch(setDefaultVariation(variation)),
})}
export default connect(mapStateToProps, mapDispatchToProps)(ProductDisplayContainer);

When the component mount, it takes the product's first variation and says it's the currently selected variation. Add to cart form components change this variation as attributes are adjusted, causing the presentational component to re-render.

Here's the presentational component.

import React from 'react';

import SimpleAddToCart from './add-to-cart/simple';
import VariationsAddToCart from './add-to-cart/variations';

const ProductDisplay = ({ product, productType, selectedVariation }) => {
    return (
        <article>
            <h1>{product.title}</h1>
            <h2>{product.id}</h2>
            {productType.multipleVariations ?
                [
                <VariationsAddToCart variations={product.relationships.variations} variation={selectedVariation} key="variations"/>] : [
                <SimpleAddToCart variation={selectedVariation} key="simple"/>
                ]}
        </article>
    )
};
export default ProductDisplay;

Boom. It's a stateless component. Know what else it is? Shadowable. It's easily shadowable by another theme or the Gatsby application because it is stateless.

Here's an example of the component being shadowed by another theme that uses the Bootstrap framework.

import React from 'react';
import DefaultLayout from '../../layouts/index'
import SEO from '../../components/seo'

import SimpleAddToCart from 'gatsby-theme-centarro-commerce/src/components/add-to-cart/simple';
import VariationsAddToCart from 'gatsby-theme-centarro-commerce/src/components/add-to-cart/variations';

const ProductDisplay = ({ product, productType, selectedVariation }) => {
    return (
        <DefaultLayout>
            <SEO title={product.title} />
            <div className={`container`}>
                <div className={`row`}>
                    <div className={`col-md-6`}>
                    </div>
                    <div className={`col-md-6`}>
                        <h1 className={`font-weight-light`}>{product.title}</h1>
                        <p className={`font-weight-bold h4`}>{selectedVariation ? selectedVariation.price.formatted: null}</p>
                        <p className={`font-weight-leight small`}>{selectedVariation ? selectedVariation.sku : null}</p>
                        <p dangerouslySetInnerHTML={{ __html: product.body.processed }}/>
                        {productType.multipleVariations ?
                            [
                                <VariationsAddToCart variations={product.relationships.variations} variation={selectedVariation} key="variations" />] : [
                                <SimpleAddToCart variation={selectedVariation} key="simple" />
                            ]}
                    </div>
                </div>
            </div>
        </DefaultLayout>
    )
};
export default ProductDisplay;

Boom! The product is now styled and we don't give a hoot about how or why we know the selected variation.

So, if you properly build your presentational and container components, you can make easily shadowed components that allow for a dynamic application built with Gatsby.