Doug Waltman

MobX + Gatsby

I've been moonlighting Gatsby projects in my free time for a couple of years now, while working with Next.js during the day. I've come to love MobX state management with Next, and wanted to bring the goodness to my Gatsby projects. Turns out that its pretty easy, and I'm going to share the approach I took in a recent project.

Simply put, MobX strutures data in "stores", which are classes primarly comprised of observable values and actions. An inject+observer pattern is used to add them to React components, which makes persisting data in our React application a breeze.

Getting started

This walkthrough assumes that you have a working Gatsby.js site. With that assumption made, let's install the modules we'll need, "mobx" and "mobx-react":

yarn add mobx mobx-react

Creating our first store

Create a new folder in the /src directory called "stores". Then, add a new file: /src/stores/messages-store.js.

Our new messages store is going to keep an array of messages to display to users, and persist them between pages. We'll create an array obserable to track messages, and actions to add new messages and clear out the queue.

// src/stores/messages-store.js

import { observable, action, decorate } from 'mobx'

class MessagesStore {
  messages = []

  addMessage(message) {
    this.messages.push(message)
  }

  clearMessages() {
    this.messages.clear()
  }

  dehydrate() {
    return {
      messages: this.messages,
    }
  }
}

decorate(MessagesStore, {
  messages: observable,
  addMessage: action,
  clearMessages: action,
})

export default MessagesStore

Providing stores to our app

We need to provide our new messages store in both gatsby-browser.js AND gatsby-ssr.js. Let's keep things dry, and create a provide-stores.js file in the root directory. We import our new store, and pass a new instance to the MobX Provider component:

// provide-stores.js

import React from 'react'
import { Provider } from 'mobx-react'
import MessagesStore from 'stores/messages-store'

export default ({ element }) => (
  <Provider messages={new MessagesStore()}>{element}</Provider>
)

Next, modify gatsby-browser.js AND gatsby-ssr.js to use the provided store like so:

// gatsby-browser.js and gatsby-ssr.js

import provideStores from './provide-stores'

export const wrapRootElement = provideStores

Using our new MessagesStore

Now that we're setup, go ahead and fire up gatsby dev and make sure the server starts and that we didn't make any mistakes.

We want to access the "messages" obserable in our new store, so let's create a new component that will allow us to add and display them. For small projects, I prefer to keep them all in a single components folder.

Our component will have an input field, two buttons, and render a list of messages. We'll take advantage of useState to store the message value from our input field, and useRef to clear the value after adding messages.

// src/components/messages.js

import React, { useRef, useState } from 'react'
import { inject, observer } from 'mobx-react'

// the "messages" prop is provided with "inject"
const Messages = ({ messages: messagesStore }) => {
  const { messages } = messagesStore
  const inputRef = useRef(null)
  const [messageValue, setMessageValue] = useState('')

  // Keep track of our new message value
  const handleInputChange = event => {
    setInputValue(event.target.value)
  }

  // Add the new message
  const handleNewMessage = () => {
    messagesStore.addMessage(messageValue)
    // clear the input field
    inputRef.current.value = ''
  }

  return (
    <div>
      <input onChange={handleInputChange} />
      <button onClick={handleNewMessage}>
        Add Message
      </button>
      <button onClick={messagesStore.clearMessages}>
        Clear Messages
      </button>
      <ul>
        {messages.map((message, m) => (
          <li key={m}>{message}</li>
        ))}
      </ul>
    </div>
  )
}

// "messages" refers to the value we gave to the <Provider />
// "observer" allows our component to react to changes to observables
export default inject('messages')(observer(Messages))

Persisting stores

At this point, try adding some messages, and then refresh the page. Oh no, Our messages are gone! #sadpanda

This is because Gatsby is a client side React application. If we want our messages to exist for the duration of our visit, including page refreshes, we'll want to persist our stores somewhere. Local storage to the rescue!

Our app will likely have more that one store at some point, so let's add helpers to move data between our app and our browser's local storage. We'll wrap them in try+catch in case our users' browsers don't support it:

// lib/local-storage.js

export const getInLocal = key => {
  try {
    const data = localStorage.getItem(key)
    return JSON.parse(data || {})
  } catch (err) {
    return {}
  }
}

export const setInLocal = (key, data) => {
  try {
    localStorage.setItem(key, JSON.stringify(data))
    return true
  } catch (err) {
    return false
  }
}

And now let's add these helpers to our messages store:

// src/stores/messages-store.js

import { observable, action, decorate } from 'mobx'
import { getInLocal, setInLocal } from 'lib/local-storage'

class MessagesStore {
  // create a key where our data will be stored
  storageKey = 'mysite.messages'

  messages = []

  constructor() {
    // first check if localStorage exists
    if (!global.localStorage) return

    // check for data in local storage
    const data = getInLocal(this.storageKey)

    // if data exists, hydrate our store
    if (data && data.messages) {
      this.messages = data.messages
    }
  }

  // allows us to save messages
  saveLocal() {
    const { messages } = this

    setInLocal(this.storageKey, { messages })
  }

  addMessage(message) {
    this.messages.push(message)
    // persist messages when they change
    saveLocal()
  }

  clearMessages() {
    this.messages = []
    // persist messages when they change
    saveLocal()
  }

  dehydrate() {
    return {
      messages: this.messages,
    }
  }
}

decorate(MessagesStore, {
  messages: observable,
  addMessage: action,
  clearMessages: action,
})

export default MessagesStore

Closing thoughts

From here, you can make improvements as the application grows. It's a good idea to create stores based on areas of concern rather than have a single store that does "everything". For example, if your Gatsby site is performing e-commerce functions, you might have a CartStore to manage items, or a NewsletterStore to manage leads.

Be wary of using object observables, as mutation won't trigger an update your components, but don't worry, that's what Observable Maps are for.

And be aware of the limitations of browser local storage group limits, and peformance when it comes to reading/writing large amounts of data you might want to persist.

MobX stores are a great way to manage shared state in a Gatsby project, and can even be used to abstract async fetch methods when working with 3rd party APIs; more on that in a future blog post. Until then, happy coding, friends.