I first realized the need for a different approach with our applications at Purple Technology when I was given a task that involved changing the CSS for a button. Unfortunately, what I thought would be a simple and quick-to-knockout task took almost an hour because the button was only visible for users at a certain stage of our onboarding process. So, after setting up the backend API to run locally, I made a new account, filled in some forms, and sat through a few loading screens before finally seeing the button I had come for.

Afterwards, I immediately thought to myself that, as a developer, I should never have to struggle just to see the page I’m supposed to be working on—especially when I only need to change some CSS. For simple applications, this problem might not be relevant, but more complex apps can be a different story. What if you have a page where users can link bank accounts and credit cards? When it’s time to redesign that page do you really need to connect to some payment provider’s API just to see the consequences of changing the colors the user sees after making a deposit? This is where decoupling and mocking the interactions for the backend is worth it.

Context Stores

For our particular solution, we decided to implement React’s built-in `useContext` hook and leverage our provider to support using mocked data. If you’re familiar with Redux, you probably already know how useful context stores and state management can be for sharing state across different components—whether that be for global state management or remote state management. The latter was already something for which my team wanted a more elegant solution; some components were growing larger than was reasonable. Unfortunately, splitting them up would have meant passing the API data and callbacks with which to change it from the parent components to the children (if you’re not familiar with this problem in React, known as prop drilling, this article has a great summary). Context stores are a great way to address these kinds of problems.

But in addition to getting around prop drilling, we found another use for a context store. By moving all of our integration logic and API calls to a context provider, we were able to instruct our application to use 100% mocked data and functions, enabling the frontend of our app to run even without an internet connection—when it otherwise needs a plethora of API integrations to see anything beyond the welcome page. For frontend developers, designers, and anyone else in your company who needs to just see your app, this is a welcome capability.

The Implementation

At Purple Technology we use Next.js, a framework for React with a file-based routing system—meaning every HTTP route corresponds to a file in a directory called `pages` at the root of the app. We decided to have two different versions of each page: the “real” page at pages/index.jsx, and the “mocked” page at pages/mocked/jsx. Inside each file, our scene is wrapped in the same provider, and switching between the two is as simple as visiting http://localhost:3000 versus http://localhost:3000/mocked.

Let’s say we have an app that welcomes the user after they sign in. We want to greet the user by name on the dashboard, so we need to make an API call to get this information. The set-up looks like this:

import React from 'react'
import DashboardProvider from 'models/dashboard'
import DashboardScene from 'scenes/Dashboard'

const DashboardPage = () => {
    return (
        <DashboardProvider>
        	<DashboardScene />
        </DashboardProvider>
    )
}

export default DashboardPage
pages/index.jsx
import React, { useState } from 'react'
import DashboardProvider from 'models/dashboard'
import DashboardScene from 'scenes/Dashboard'

const DashboardPage = () => {
    const initial = {
    	name: 'John Doe',
    }
    const [data, setData] = useState(initial)
    const changeName = () => {
    	return setData({ name: 'John Smith' })
    }
    const dashboardValue = { data, changeName }
    
    return (
        <DashboardProvider value={dashboardValue}>
        	<DashboardScene />
        </DashboardProvider>
    )
}

export default DashboardPage
pages/mocked.jsx

You can see that we’re using the page component to determine where the data comes from. Do we want to use some hardcoded mocked data, or make real API calls? If the former, we just pass in the data we want. Otherwise, the provider behaves as normal, which looks like this:

import React, { createContext, useState, useEffect } from 'react'

export const context = createContext()

const Provider = ({ value, children }: { value; children }) => {
    const [data, setData] = useState(null)
    
    const loadData = async () => {
    	const { data } = await fetch('https://someapiendpoint.com')
    	setData(data.json())
    }
    
    const changeName = async () => {
    	const { data } = await fetch('https://someapiendpoint.com', {
            method: 'POST',
            body: JSON.stringify({ name: 'John Smith' })
        })
    	setData(data.json())
    }
    
    useEffect(() => {
    	loadData()
    }, [])
    
    return (
        <context.Provider value={value ? value : { data }}>
        {children}
        </context.Provider>
    )
}

export default Provider
models/dashboard.jsx

As you can see, if the provider is given some data from the outside—in our case the hardcoded, mocked data—it simply passes it through, skipping over all the API key-requiring steps in the interim. Otherwise, it gathers data from our external services.

And as for using the data in the implementing components, it’s as simple as:

import React, { useContext } from 'react'
import { context } from ‘models/dashboard'

const DashboardScene = () => {
	const { data, changeName } = useContext(context)
    return (
        <div>
            <Welcome name={data.name} onClick={() => changeName()} />
            <button onClick={() => changeName()}>Change Name</button>
        </div>
    )
}
scenes/dashboard.jsx

Is it worth it?

So, why go through all this extra trouble? Why not just create the testing accounts and fill in onboarding the forms until the accounts are in the state you need them to be in? Because DX matters. If your application stack is both complex and painful to work with, the features your business needs added to it will come out slowly and painfully. On the other hand, if it’s fun to work on, your developers will volunteer to step up and crank out those mission-critical features the business requested at the last minute.

But you should also want your non-developers to easily access your company’s product. If you deploy your application with these mocked pages to some staging instance, anyone in your company will be able to quickly provide UX feedback, spot typos, answer customer support questions, and so on. Not only can your developers benefit from this, but possibly some other departments of your company. If your app is complex enough, it might be worth mocking the frontend.