Storybook and Mock Service Worker to create a prototype list of news articles for a React application that interfaces with a GraphQL API.
Storybook is a powerful tool for creating and testing UI components in isolation. It will help us focus on the building and iterating of individual components without having to worry about the larger context of the app.
Mock Service Worker is a library for mocking out a GraphQL API and testing app GraphQL queries and mutations in isolation. It is a powerful tool for enhancing prototypes, and will be especially useful in a scenario like this where we don't have access to a deployed API.
Storybook needs to be installed in a project that is already setup with a framework, so before we dive into building our prototype we’ll want to spin up a new React project. We’ll be working from this Mantine Vite (Minimal) Template throughout this tutorial. So, grab a copy of the template and follow the steps below.
First of all we’ll need to add Storybook to our project. Storybook provides a CLI allowing us to get setup with a single line command. From your project’s root directory run the following command in your terminal:
yarn dlx storybook@latest init
If all goes well, you should see a setup wizard that provides you with a short tour of Storybook’s main concepts and features.
You’ll notice a couple of new folders in your project, .storybook
and src/stories
. .storybook
is where Storybook’s configuration files are saved. src/stories
contains some example component stories, which you can leave for reference when creating stories of your own or if you wish, delete this folder entirely.
As we’re using the Mantine UI Component Library in our project, we’ll also need to setup Mantine in Storybook before creating our first story.
Install the Storybook add-on:
yarn add --dev storybook-dark-mode
Add storybook-dark-mode
add-on to .storybook/main.ts
:
import type { StorybookConfig } from '@storybook/react-vite';
import { join, dirname } from 'path';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
// ...config
addons: [
// ...addons
getAbsolutePath('storybook-dark-mode'),
],
};
export default config;
Save the .storybook/preview.ts
as .storybook/preview.tsx
and replace the content with the following:
// Import styles of packages that you‘ve installed.
// All packages except `@mantine/hooks` require styles imports.
import '@mantine/core/styles.css';
import React, { useEffect } from 'react';
import { addons } from '@storybook/preview-api';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
import { MantineProvider, useMantineColorScheme } from '@mantine/core';
const channel = addons.getChannel();
function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
const { setColorScheme } = useMantineColorScheme();
const handleColorScheme = (value: boolean) =>
setColorScheme(value ? 'dark' : 'light');
useEffect(() => {
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
}, [channel]);
return <>{children}</>;
}
export const decorators = [
(renderStory: any) => (
<ColorSchemeWrapper>{renderStory()}</ColorSchemeWrapper>
),
(renderStory: any) => <MantineProvider>{renderStory()}</MantineProvider>,
];
As we don’t have an API to fetch a list of news articles from as yet, we’ll create a “hard-coded” set of mock articles to represent the data we expect to receive from the API. Create a new file called src/mocks/articles.ts
:
export const MOCK_ARTICLE_LIST_DATA = [
{
id: 1,
date: '1st January 2024',
slug: 'article-one',
title: "Golf’s Worst Kept Secrets: Why We Really Wear Funny Pants",
imageUrl:
'https://images.unsplash.com/photo-1519682271141-57c25ad60410?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=540&ixid=MnwxfDB8MXxyYW5kb218MHwxMjAxNTQ0NXx8fHx8fHwxNzE0NjI4ODEy&ixlib=rb-4.0.3&q=80&w=720',
},
{
id: 2,
date: '10th February 2024',
slug: 'article-two',
title: 'Golf Cart Grand Prix: When Tee Time Turns into a Race',
imageUrl:
'https://images.unsplash.com/photo-1602991174407-a015b35a7b00?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=540&ixid=MnwxfDB8MXxyYW5kb218MHwxMjAxNTQ0NXx8fHx8fHwxNzE0NjI4ODUx&ixlib=rb-4.0.3&q=80&w=720',
},
{
id: 3,
date: '12th March 2024',
slug: 'article-three',
title:
"Swing Like a Pro: How to Look Good Even When You’re Missing Every Shot",
imageUrl:
'https://images.unsplash.com/photo-1684144064253-bb3b4c8fc700?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=540&ixid=MnwxfDB8MXxyYW5kb218MHwxMjAxNTQ0NXx8fHx8fHwxNzE0NjI4ODEy&ixlib=rb-4.0.3&q=80&w=720',
},
// ...articles
]
Subscribe for updates
We send a newsletter from time to time when we publish new resources, articles, and open source projects on the topics of software design and engineering, design systems, and process & practice.
Now that we have Storybook up and running in our React project along with some mock data, we can get into prototyping our news article components. We’re going to need a couple of UI components for our list of news articles: An ArticleCard
to display a news articles overview, and an ArticleList
to render our news articles in a grid.
src/components/ArticleCard.tsx
import { AspectRatio, Card, Image, Text } from '@mantine/core';
type Props = {
slug: string;
date: string;
title: string;
imageUrl: string;
};
export const ArticleCard = ({ slug, date, title, imageUrl }: Props) => {
return (
<Card p="md" radius="md" component="a" href={`#/articles/${slug}`}>
<AspectRatio ratio={720 / 540}>
<Image src={imageUrl} />
</AspectRatio>
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mt="md">
{date}
</Text>
<Text fw="bold" mt={5}>
{title}
</Text>
</Card>
);
};
src/components/ArticleList.tsx
import { Container, SimpleGrid } from '@mantine/core';
import { ArticleCard } from './ArticleCard';
type Props = {
articles: {
id: number;
slug: string;
date: string;
title: string;
imageUrl: string;
}[];
};
export const ArticleList = ({ articles }: Props) => {
return (
<Container py="xl">
<SimpleGrid cols={{ base: 1, sm: 2 }}>
{articles.map((article) => (
<ArticleCard key={article.id} {...article} />
))}
</SimpleGrid>
</Container>
);
};
Alongside our ArticleCard
and ArticleList
components we now create a “story” file for each one like so:
src/components/ArticleCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ArticleCard } from './ArticleCard';
const meta: Meta<typeof ArticleCard> = {
component: ArticleCard,
};
export default meta;
type Story = StoryObj<typeof ArticleCard>;
export const Default: Story = {
args: {
date: '1st January 2024',
slug: 'article-one',
title: "Golf’s Worst Kept Secrets: Why We Really Wear Funny Pants",
imageUrl:
'https://images.unsplash.com/photo-1519682271141-57c25ad60410?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=540&ixid=MnwxfDB8MXxyYW5kb218MHwxMjAxNTQ0NXx8fHx8fHwxNzE0NjI4ODEy&ixlib=rb-4.0.3&q=80&w=720',
},
render: (args) => <ArticleCard {...args} />,
};
src/components/ArticleList.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ArticleList } from './ArticleList';
import { MOCK_ARTICLE_LIST_DATA } from '../mocks/articles';
const meta: Meta<typeof ArticleList> = {
component: ArticleList,
};
export default meta;
type Story = StoryObj<typeof ArticleList>;
export const Default: Story = {
args: {
articles: MOCK_ARTICLE_LIST_DATA,
},
render: (args) => <ArticleList {...args} />,
};
Now when we start up our Storybook using the yarn storybook
command, we’ll be able to view and interact with our components.
Having our components available in Storybook provides an excellent way of testing, reviewing and iterating on them without having to integrate them into the application itself. We are however limited in our analysis of the components by the fact that they’re currently displaying hard-coded mock data. There’s no sense of what might happen if this data took a while to load, if the API was unable to return any data, or if the API returned an error. Let’s take a look at alleviating these shortfalls by setting up a Mock GraphQL API using Mock Service Worker and Apollo.
When interacting with a GraphQL API it can be beneficial to leverage the power of a state management library such as Apollo Client to help simplify the handling of remote and local data.
Install Apollo Client Dependencies
yarn add @apollo/client graphql
This will install the two dependencies required to use Apollo Client in our application: Apollo Client, which includes everything required to run itself; and GraphQL, which provides the logic required to parse GraphQL Queries and Mutations.
We’ll also need to setup an ApolloClient
instance that we can use to interact with the Mock API we’re going to be creating shortly.
Inside our “mocks” folder, create a new file called src/mocks/MockApolloProvider.tsx
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
type MockApolloProviderProps = {
children: React.ReactElement;
};
const client = new ApolloClient({
uri: '//fake.gql.server',
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
},
});
export const MockApolloProvider = ({ children }: MockApolloProviderProps) => {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
Let’s go ahead and install Mock Service Worker into our project’s development dependencies:
yarn add --dev msw
Mock Service Worker is designed to intercept any client-side requests our application makes, and mock out a response. To enable this behaviour in the browser, we’ll need to create and register a service worker in our application. The great thing about the Mock Service Worker library is that we don’t actually have to write any code to create this worker ourselves, the library provides a CLI that generates a worker file into our application’s public
directory, and registers it in our package.json
:
yarn dlx msw init public/ --save
Let’s say in our final application we’re expecting that a list of news articles will be populated by executing a GraphQL Query named GetNewsArticles
. To enable the simulation of this query request and possible responses in Storybook, we’ll need to write a mock request handler, and resolver for the GetNewsArticles
query.
Go ahead and create a new file src/mocks/handlers.ts
with the following code:
import { graphql, HttpResponse } from 'msw'
import { MOCK_ARTICLE_LIST_DATA } from './articles';
export const handlers = [
// Intercepts "GetNewsArticles" graphql query.
graphql.query('GetNewsArticles', () => {
return HttpResponse.json({
data: {
// Return MOCK_ARTICLE_LIST_DATA as the "articles" root-level property.
articles: MOCK_ARTICLE_LIST_DATA,
},
});
}),
];
With our mocked request and response in place, we now need to configure Mock Service Worker to intercept any requests for GetNewsArticles
made in Storybook and resolve them using our mocked handler. Storybook has a convenient add-on for Mock Service Worker that handles the configuration and initialisation of the service worker for us.
Install the storybook-msw-addon
using the following command:
yarn add --dev storybook-msw-addon
And register storybook-msw-addon
to ./storybook/main.ts
const config: StorybookConfig = {
// ...config
addons: [
// ...addons
getAbsolutePath('storybook-msw-addon'),
],
};
export default config;
Then update our ./storybook/preview.tsx
to initiate the Mock Service Worker and include the mswLoader
:
// ...imports
import { initialize, mswLoader} from 'storybook-msw-addon';
// initialise MSW addon.
initialize({
onUnhandledRequest: 'bypass',
});
// provide the MSWloader globally.
export const loaders = [
//...loaders
mswLoader
];
After running yarn storybook
and viewing the browser’s console you should see the message: [MSW] Mocking enabled
. This indicates that the service worker is in place and listening for any requests to intercept.
Finally we must now wrap our stories with the MockApolloProvider
, enabling our components to interact with the mock API. Add the following code to ./storybook/preview.tsx
:
// ...imports
import { MockApolloProvider } from '../src/mocks/MockApolloProvider';
export const decorators = [
// ...decorators
(renderStory: any) => <MockApolloProvider>{renderStory()}</MockApolloProvider>
];
With the setup behind us, all that’s left for us to do is configure the ArticleList
component to make an API request to fetch the articles.
In GraphQL we describe our expected response in a query declaration. Let’s add a declaration to our src/components/ArticleList.tsx
for the GetNewsArticles
query:
import { gql } from '@apollo/client'
const GET_NEWS_ARTICLES = gql`
query GetNewsArticles {
articles {
id
date
slug
title
imageUrl
}
}`;
Then we can use Apollo’s useQuery
hook to execute the API request in src/components/ArticleList.tsx
:
// ...imports
import { gql, useQuery } from '@apollo/client'
import { Container, SimpleGrid } from '@mantine/core';
import { ArticleCard } from './ArticleCard';
// ...GET_NEWS_ARTICLES
type Article = {
id: number;
slug: string;
date: string;
title: string;
imageUrl: string;
};
export const ArticleList = () => {
const { data, loading, error } = useQuery(GET_NEWS_ARTICLES);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<Container py="xl">
<SimpleGrid cols={{ base: 1, sm: 2 }}>
{data.articles.map((article: Article) => (
<ArticleCard key={article.id} {...article} />
))}
</SimpleGrid>
</Container>
);
};
In the above example code, the useQuery
hook is providing us with the returned data object, a loading boolean, and an error object. We’re then able to use these properties to conditionally render either a loading indicator, error notice, or the list of news articles.
We’ll also want to update our src/components/ArticleList.stories.tsx
to reflect the above changes:
import type { Meta, StoryObj } from '@storybook/react';
import { handlers } from '../mocks/handlers';
import { ArticleList } from './ArticleList';
const meta: Meta<typeof ArticleList> = {
component: ArticleList,
};
export default meta;
type Story = StoryObj<typeof ArticleList>;
export const Default: Story = {
render: () => <ArticleList />,
};
Default.parameters = {
msw: {
handlers
},
};
Note that we are now referencing the Mock Service Worker handlers. Fire up your Storybook and we should see something like this:
ArticleList component loading mock data in Storybook
And there we have it, a complete news article list that mimics the behaviour of a production ready component with no need for a deployed API to be in place. Of course we’ve only scratched the surface here of what’s possible when mocking out an API using Mock Service Worker. Why not go ahead and look at extending this example with more advanced GraphQL operations?
If you’re looking to get more from your prototyping efforts, we can help.
article
· 11 min readarticle
· 16 min readtalk
article
·article
· 4 min readarticle
· 7 min readarticle
· 11 min readarticle
· 16 min readtalk
article
·article
· 4 min readarticle
· 7 min readarticle
· 11 min readarticle
· 16 min readarticle
·talk
article
· 7 min readHave a chat with one of our co-founders, Jed or Boris, about how Thinkmill can support your organisation’s software ambitions.
Contact us