Fixture-first Development
When you hear the word "Storybook," you probably think UI libraries. Tools like Storybook and Cosmos have been around for a few years now and do a pretty awesome job of presenting UI components in isolation. What most don't consider, however, is how these tools can go beyond just presenting UI components.
Let's talk about this!
Thinking in state
Consider the typical Button component in a UI library. When designing or implementing this component, one of the key considerations we ask ourselves is
"What states will this button have?"
Things might start off with a few simple states such as default and disabled.
Then comes the interactive states such as hovered and active...
Then primary and secondary...
Then primary disabled hovered and secondary disabled hovered
Before you know it, you have many states to consider and keep tracking.
This is when creating fixtures (or stories) starts to provide some real benefit. A fixture is a way of fixing the state of a component and modelling it in a browser environment. By doing this, we document our many states and also provide a means for quickly reproducing them during development and testing.
Compositional components
Moving higher up the component tree, it's easy to lose this state-first way of thinking about components. As scope increases, the core responsibilities of components don't change
- Rendering output
- Triggering side effects
While fixtures don't always help us demonstrate side effects, we can always use them as a means of modelling state.
Working in isolation
One of the first pages in the official React docs - Components and Props - states the following.
Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.
Somewhere along the way, I think we forgot this is why we, as a community, chose to use React and not a page-scoped solution such as jQuery.
While focusing on integration is clearly important, there's huge value in being able to change and test components in isolation.
What this looks like
Here's an example of a Page component that has many states that are dependent on a network request and its response:
Believe it or not, everything you see above was made in total isolation and without spinning up the full site. Note how we can simulate states of our GraphQL client such as fetching and error without any magic — just fixtures and state.
Because React allows us to think of each piece in isolation, there's much less overhead required to do front-end work than you might think. Sure, we eventually need to bring everything together, but that is a small part of the whole development process.
Creating Fixtures
Depending on what tool you choose to use, the way in which you create fixtures will differ, but the process will almost always be the same.
1. Find the component you want to work on
Each project is different but you'll likely want to create fixtures for macro-components such as pages, forms, cards and modals.
For this example let's assume we're working with a page component that makes a GraphQL request and presents the state of that request to the user.
export const PostsPage = () => { const [getPostsState, refetch] = useQuery({ query: gql` query GetPosts { posts { id title content } } ` }); if (getPostsState.fetching) { return ( <ContentCentered> <Spinner /> </ContentCentered> ); } if (getPostsState.error) { return ( <ContentCentered> <Icon type="warning" /> <h1>Error</h1> <p>{getPosts.error.message}</p> </ContentCentered> ); } if (getPostsState.data.posts.length === 0) { return ( <ContentCentered> <Icon type="empty" /> <h1>No posts found!</h1> </ContentCentered> ); } return ( <Content> {getPostsState.data.posts.map( post => <PostCard key={post.id} {...post} /> )} </Content> ); };
2. Setup the props and contexts for all key states
Once a component has been decided, it's time to work out what key states would be useful to have in a fixture. In our case, the key states of this page component are
- Fetching
- Error
- Empty list
- Populated list
Here's an example fixture that mocks the key states noted above for the PostsPage component:
const fetchingState = { executeQuery: () => { fetching: true }, }; const errorState = { executeQuery: () => { error: new Error("Something went wrong") }, }; const emptyState = { executeQuery: () => { data: { posts: [] } }, }; const dataState = { executeQuery: () => { data: { posts: [{ id: 1, name: "My post" }] } }, }; export default { fetching: ( <GraphqlProvider value={fetchingState}> <PostsPage /> </GraphqlProvider> ), error: ( <GraphqlProvider value={errorState}> <PostsPage /> </GraphqlProvider> ), empty: ( <GraphqlProvider value={emptyState}> <PostsPage /> </GraphqlProvider> ), data: ( <GraphqlProvider value={dataState}> <PostsPage /> </GraphqlProvider> ) }
Since hooks have replaced high-order components, you're going to find yourself mocking contexts more often, so get used to it!
Note: Most libraries don't document how to mock their context so you might have to dive into some code (or do some console.logs) to find out what the different states of the context look like.
We've now added dedicated documentation on mocking out context to the urql docs and encourage other library authors to do the same!
3. Develop within those fixtures
Once your fixtures are in place, you can test, style, and change logic within components quickly and without distraction! 🎉
Fixtures can also be used for automated testing such as visual regression, component snapshots, and functional testing.
Be mindful of changes that should be tested on a site-wide deployment, such as changes to network requests, hooking into new context, or just adding a component to the site for the first time. As mentioned earlier, this won't be too often, but in these cases, integration testing is the way to go.
Finding out more
Hopefully, if you've gotten this far, you're interested in trying this out for yourself!
I've put together an example repo that contains source code and live examples (including those used in this post) demonstrating the use of fixtures in a real-world project.
Examples include:
- Fixtures of network requests & responses
- Fixtures of modals, forms, and validation
- Fixtures of ui components
- Visual regression testing (using... you guessed it, fixtures)
Also, a huge shoutout to the contributors of the project React Cosmos who have made a great tool and documentation on developing with fixtures!