Use the React Context API with Gatsby
I'm a bit late to the party using the new React Context API, I did get to use it the other day at work, I also made a snippet to scaffold out a component for it.
I had followed a couple of guides explaining how to use it and neither of them as good as this great explanation of how to use it from @leighchalliday, thank you Leigh π It's a great use case which helped me understand how to use it.
So, after doing this in a CRA project I decided to use it on one of my Gatsby projects. With Gatsby the layout is slightly different where you can have multiple layouts for differing sections of your app, so this lends well for passing context.
One thing to bear in mind is that this is for my specific use case so I'll apologise upfront now if any of this is confusing, I'm trying to get this documented to help me understand it too π
With Gatsby if you want to use the React 16.3 then you will need to install the Gatsby React next plugin:
1npm install gatsby-plugin-react-next
Gatsby uses React 16.2 I believe so you will need to use this plugin.
Another thing you may need to do is:
1npm i react react-dom2npm un react react-dom
This may be because I was trying to use it in an old project, I've had
to do this on three projects now as I was getting createContext
is
not a function errors until I did this.
One other thing you may want to consider if it appears nothing is
working try using the npm ci
command. This is the npm 6+ version of
deleting your node_modules
folder and reinstalling. π€
So let's go through one of my favourite use cases at the moment and add theming support to a Gatsby site and use the React context API to manage the theme.
You can see how theme a React app without the React Context API in my styled-components π getting started post.
For illustration I'll go over it here now, you add the ThemeProvider
at the highest level in the application structure so that all
descendants/children of the app can access it.
I have already done this for my personal site and now I'm going to do it here so let's go through it together.
Let's make a component!
Ok, so everything in React is a component, that's why I like it so
much - so let's make a SomethingContext.js
component, as I want to
do the things with the styled-components π
Let's start by giving it an imaginative name:
1touch src/layouts/components/BlogThemeContext.js
There we go π
Ok, the 'things' I want to do with the Context API are:
- change the styled-components
ThemeProvider
- rotate the site hero patterns
Now to scaffold out the context component, I have already mentioned
the VS Code snippet for my own personal use which is the basic
structure for the Context
which is in two parts, a Provider
and a
Consumer
Let's create the Context
and the Consumer
in this component.
Using the snippet it should look something like this:
src/layouts/components/BlogThemeContext.js
1import React from 'react'2// Context is made up of two things3// Provider - Single as close to top level as possible4// Consumer - Multiple have multiple consumers5export const BlogThemeContext = React.createContext()67export class BlogThemeProvider extends React.Component {8 state = {9 item1: 1,10 item2: 2,11 }1213 // add function here14 functionHere = () => {15 this.setState({16 item1: 2,17 item2: 3,18 })19 }20 render() {21 return (22 <BlogThemeContext.Provider23 value={{24 ...this.state,25 functionHere: this.functionHere,26 }}27 >28 {this.props.children}29 </BlogThemeContext.Provider>30 )31 }32}
So the props
passed into the <BlogThemeContext.Provider>
is the
state and the methods contained in BlogThemeProvider
these can then
be accessed throughout the app by using the
<BlogThemeContext.Consumer>
.
Now let's add the BlogThemeProvider
at the top level of our app so
that the state and functions of the provider can are accessible for
the children of the layout/index.js
.
This is what it looks like before adding the context provider, you'll
notice that the styled-components ThemeProvider
is a top level
component here.
src/layouts/index.js
1const TemplateWrapper = ({ children }) => (2 <ThemeProvider theme={theme}>3 <PageContainer>4 <Helmet title={nameContent} meta={siteMeta} />5 <Header />6 <Main>{children()}</Main>7 <Footer />8 </PageContainer>9 </ThemeProvider>10)
Now we already have the styled-components ThemeProvider
which
receives a theme
object, and we want to manage the theme in our
context provider. So let's import the existing theme from the
globalStyle
module into BlogThemeContext
and add theme
to the
state of the BlogThemeProvider
:
1import React from 'react'2import PropTypes from 'prop-types'34import { theme } from '../../theme/globalStyle'56// Context is made up of two things7// Provider - Single as close to top level as possible8// Consumer - Multiple have multiple consumers9export const BlogThemeContext = React.createContext()1011export class BlogThemeProvider extends React.Component {12 state = {13 theme,14 }1516 // add function here17 functionHere = () => {18 this.setState({19 item1: 2,20 item2: 3,21 })22 }23 render() {24 return (25 <BlogThemeContext.Provider26 value={{27 ...this.state,28 functionHere: this.functionHere,29 }}30 >31 {this.props.children}32 </BlogThemeContext.Provider>33 )34 }35}3637BlogThemeProvider.propTypes = {38 children: PropTypes.any,39}
While we're here let's also add the function to handle the theme
changing by replacing the dummy functionHere
function in the snippet
and also bring in the themes we want to switch between.
1import React from 'react'2import PropTypes from 'prop-types'34import { theme1, theme2 } from '../../theme/globalStyle'56export const BlogThemeContext = React.createContext()78export class BlogThemeProvider extends React.Component {9 state = {10 theme,11 }1213 handleThemeChange = e => {14 let theme = e.target.value15 theme === 'theme1' ? (theme = theme1) : (theme = theme2)16 this.setState({ theme })17 }18 render() {19 return (20 <BlogThemeContext.Provider21 value={{22 ...this.state,23 handleThemeChange: this.handleThemeChange,24 }}25 >26 {this.props.children}27 </BlogThemeContext.Provider>28 )29 }30}3132BlogThemeProvider.propTypes = {33 children: PropTypes.any,34}
Use the Context.Consumer
So, now, let's use it, right? The way to use is much like with the
styled-component ThemeProvider
, import your <ThemeSelectProvider>
then you can use the <ThemeSelectContext.Consumer>
to access the
functions and state of the BlogThemeContext
via the
<ThemeSelectProvider>
The child of a consumer is a function, so rather than have your app being returned like you would with a normal React component like this:
1<Wrapper>2 <Child />3</Wrapper>
So you need to embed a function like this:
1<Wrapper>{() => <Child />}</Wrapper>
So you're returning the (in this example, <Child />
) app as the
result of the <Context.Consumer>
function, here we can also get any
of the properties or state from the Context, in my use case here I
want to get the theme
prop out of the Context provider value
(<BlogThemeProvider>
) so we'll use ES6 destructuring to pull out the
theme
object.
The styled-components ThemeProvider
can now use the theme
object
supplied by the <BlogThemeContext.Consumer>
so it's safe to remove
the import from globalStyle
.
1const TemplateWrapper = ({ children }) => (2 <BlogThemeProvider>3 <BlogThemeContext.Consumer>4 {({ theme }) => (5 <ThemeProvider theme={theme}>6 <PageContainer>7 <Helmet title={nameContent} meta={siteMeta} />8 <Header />9 <Main>{children()}</Main>10 <Footer />11 </PageContainer>12 </ThemeProvider>13 )}14 </BlogThemeContext.Consumer>15 </BlogThemeProvider>16)
There's also a template src/template/blog-posts.js
which Gatsby uses
to generate the posts in this blog, let's make it the same, let's wrap
the app in the return function for the context consumer, before it
looked like this:
1const Template = ({ data, pathContext }) => {2 const { markdownRemark: post } = data3 const { frontmatter, html } = post4 const { title, date } = frontmatter5 const { next, prev } = pathContext67 return (8 <PostWrapper border={({ theme }) => theme.primary.light}>9 <Helmet title={`${title} - blog.scottspence.me`} />10 <Title>{title}</Title>11 <TitleDate>{date}</TitleDate>12 ....
Now it looks like this:
1const Template = ({ data, pathContext }) => {2 const { markdownRemark: post } = data3 const { frontmatter, html } = post4 const { title, date } = frontmatter5 const { next, prev } = pathContext67 return (8 <BlogThemeProvider>9 <BlogThemeContext.Consumer>10 {({ theme }) => (11 <PostWrapper border={({ theme }) => theme.primary.light}>12 <Helmet title={`${title} - blog.scottspence.me`} />13 <Title>{title}</Title>14 <TitleDate>{date}</TitleDate>15 ....
Add a ThemeSelect component
The ThemeSelect
component is a select component I have used several
times now, here's the source from my personal site, it's what we're
going to use to handle the theme change, it will use the
handleThemeChange
method in the BlogThemeContext
so we better use
a Context consumer to access the method:
src/layouts/components/Footer.js
1<BlogThemeContext.Consumer>2 {({ handleThemeChange }) => (3 <ThemeSelectWrapper>4 <ThemeSelect handleThemeChange={handleThemeChange} />5 </ThemeSelectWrapper>6 )}7</BlogThemeContext.Consumer>
Now if we have a look at the state
in the React dev tools we can see
the font changing with the selection of the theme change, much like in
the styled-components π
getting started post.
Ok, success π― π now onto the background switching/transition thingy.
Switch hero (background patterns)
So, right now to switch between Steve Schoger's awesome hero
patterns I have a function sitting in the globalStyle
module which
returns a random HERO pattern:
1export const randoHero = () => {2 const keys = Object.keys(HERO)3 return HERO[keys[(keys.length * Math.random()) << 0]]4}
This function sets the background on the body
each reload with a
random key from the HERO
object, now I'm going to move that to the
componentDidMount()
of BlogThemeContext.Provider
so (for now) it
selects a random key from the object every ten seconds:
1export class BlogThemeProvider extends React.Component {2 state = {3 theme: theme1,4 background: HERO[0]5 }67 handleThemeChange = e => {8 let theme = e.target.value9 theme === 'theme1' ? (theme = theme1) : (theme = theme2)10 this.setState({ theme })11 }1213 componentDidMount() {14 this.interval = setInterval(() => {15 const keys = Object.keys(HERO)16 const background =17 HERO[keys[(keys.length * Math.random()) << 0]]1819 this.setState({ background })20 }, 10 * 1000)21 }2223 render() {24 ....
Now to find somewhere in the app that can change the background! As I
mentioned before the background for the page was set on the body
via
styled-components injectGlobal
I now want to access the background
prop from Context so I have moved this to src/layouts/index.js
. I
already have the Context consumer added for the theme
so let's
destructure the background
prop out as well:
1const TemplateWrapper = ({ children }) => (2 <BlogThemeProvider>3 <BlogThemeContext.Consumer>4 {({ theme, background }) => (5 <ThemeProvider theme={theme}>6 <PageContainer background={background}>7 <Helmet title={nameContent} meta={siteMeta} />8 <Header />9 <Main>{children()}</Main>10 <Footer />11 </PageContainer>12 ....
Now use the background
prop in the main wrapper PageContainer
We're passing the both the background image and colour as styled-component props now.
1const PageContainer = styled.div`2 background-color: ${props => props.theme.background};3 background-image: url("${props => props.background}");4 background-attachment: fixed;
That's it! We have used the React Context API to access state and use it at (two) different points in the app.
Thanks for reading π
Thanks you for looking at all the code walls!
If you have any feedback please get in touch
You can find all the source code to this on my repo for this project here: https://thelocalhost.io/