Using React Context in Gatsby Projects

Posted: June 5th, 2019

Topics: React, Gatsby, Context, Styled-Components

I'll admit, it took me a while to finally adopt both React context & hooks in my day-to-day React vocabulary. Like most things though, after many eyerolls, once I gave them both a chance, it's become hard to turn back.

I'd like to share a pretty common pattern I've been using for Gatsby projects, which is a modal. The meat of this thing is a modal context which can be accessed globally by whichever components can make use of it. I'll put it somewhere like src/store/modalContext. Here's what that could look like:

import React from 'react';
import PropTypes from 'prop-types';

const defaultState = {
  open: false,
};

const ModalContext = React.createContext(defaultState);

class ModalProvider extends React.Component {
  state = {
    open: false,
  };

  static propTypes = {
    children: PropTypes.node.isRequired,
  };

  close = () => {
    this.setState({ open: false });
  };

  open = () => {
    this.setState({ open: true });
  };

  render() {
    const { children } = this.props;
    const { open } = this.state;
    return (
      <ModalContext.Provider
        value={{
          open,
          closeModal: this.close,
          openModal: this.open,
        }}
      >
        {children}
      </ModalContext.Provider>
    );
  }
}

export default ModalContext;

export { ModalProvider };

This alone isn't really going to do much. It's essentially a HOC which offers the open, closeModal & openModal props to whatever children are passed through in. So, it's a vehicle for our modal component.

Our modal component could look something like this:

import React, { useMemo } from 'react';
import PropTypes from 'prop-types';

import ModalContext from '../../store/modalContext';
import { ModalWrapper, ModalInner } from './Modal.css';

const Modal = ({ open }) => {
  const handleLifeCycle = () => {
    //eslint-disable-next-line no-console
    console.log('Lifecycle methods?  F*ck yeah.');
  };
  // Each time open prop changes, run handleLifeCycle()
  useMemo(handleLifeCycle, [open]);

  return (
    <ModalContext.Consumer>
      {({ closeModal }) =>
        open ? (
          <ModalWrapper onClick={closeModal}>
            <ModalInner>Hello</ModalInner>
          </ModalWrapper>
        ) : null
      }
    </ModalContext.Consumer>
  );
};

Modal.propTypes = {
  open: PropTypes.bool.isRequired,
};

export default Modal;

Now, in my latest project, I wanted to make use of componentDidMount because I'm doing some not-so-standard things with the modal. In 7/10 modal situations I find myself wanting to have the mount-time action available to me. So instead of being lame 2017 ducks, how could we do this within the context/hooks ecosystem? I'd say useEffect might be the way to go here, but we actually haven't written any hooks within this component from which to rely on effects.

What we can do here, however, is take advantage of useMemo - which is similar to useEffect - but takes specific parameters which tell it when to fire. It's also more resource efficient, but it comes at a cost. The function handleLifeCycle will run when the parameter open sees a change in value. However, if the new value is the same as the previous value, it will not run. This could be problematic if you're working with some sort of function that has the possibility of returning the same value on subsequent instantiations/requests. We're using a boolean here, so it's a moot point, but keep that in mind in your own interpretations.

Now, we need to render this somewhere. This is a global component, so let's stick it in a HOC like Layout.js.

Here's mine:

import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import { ThemeProvider } from 'styled-components';
import { SiteWrapper } from './Layout.css';
import Reboot from '../../styles/Reboot';
import Global from '../../styles/Global';
import Theme from '../../styles/Theme';
import ModalContext from '../../store/modalContext';
import Modal from '../../components/Modal';

const Layout = ({ children }) => {
  return (
    <SiteWrapper>
      <Reboot />
      <Global />
      <ThemeProvider theme={Theme}>
        <Fragment>
          <Header />
          {children}
          <Footer />
  
          <ModalContext.Consumer>
            {({ open }) => {
              return <Modal open={open}/>;
            }}
          </ModalContext.Consumer>
        </Fragment>
      </ThemeProvider>
    </SiteWrapper>
  );
};

Layout.propTypes = {
  children: PropTypes.node.isRequired,
};

export default Layout;

Finally, you're going to need to wrap your entire root component with the provider in order for it to be accessible. In gatsby-browser.js like this:

import React from 'react';
import { ModalProvider } from './src/store/modalContext';

// eslint-disable-next-line react/prop-types
export const wrapRootElement = ({ element }) => {
  return <ModalProvider>{element}</ModalProvider>;
};

And now we're done. We've got a global component which global actions without Redux. In order to trigger this modal, you'd just need to grab the openModal object and inject it into a button or something wrapped with the context consumer. Like this:

    <ModalContext.Consumer>
      {({ openModal }) => {
        return <button onClick={openModal}>Open Modal</button>;
      }}
    </ModalContext.Consumer>

All of this is baked into my opinionated Gatsby DatoCMS Starter.

Enjoy!