Take the Gatsby out of my Storybook

Philipp Melab / Aug 31, 2021

Our workflow for front-end and UX development heavily relies on Storybook to be able to quickly iterate on prototypes and ideas. But we regularly faced problems when it came to dependencies to the React framework in use, be it Gatsby, Next.js or whatever was en vogue. The road to our eventual solution is one worthy to share.

The Problem

Storybook is a great tool to quickly create React components, showcase them under different circumstances and test them in isolation. We also use Chromatic to coordinate reviews and run visual regression tests. That all works great, but when framework dependencies like <GatsbyImage/>, <Link/> or useStaticQuery came into play, things always went south quickly.

Just have a look at the Gatsby documentation page on visual testing with Storybook:

... be advised that Storybook relies on webpack 4, and Gatsby is currently supporting webpack 5 ...

... Transpile Gatsby module because Gatsby includes un-transpiled ES6 code ...

... Use babel-plugin-remove-graphql-queries to remove static queries from components when rendering in storybook ...

And we did not even get to the part where they tell us to modify the global window object to make links not break in Storybook.

If there is one thing worse than maintaining a webpack configuration it’s maintaining two of them. Ideally, we also run Jest which comes with its own preprocessing, and if you then want to add additional webpack plugins on either Gatsby or Storybook side while keeping all of them operational, the whole endeavour becomes an exercise in futility.

Rapid iterations are a (if not the) key metric of a successful development process. The "rapid" part of "rapid iterations" is crucial if you need to stay within the average attention span of product owners and UX experts during mob programming sessions. "I'll quickly fix my webpack config ..." is a lie, and we all know that.

Granted, Next.js' architecture is in a better state in that regard, but it does not solve the ultimate problem. As soon as you put one of these into a component ...

... it will forever be bound to this framework. Fortunately, the ecosystem is maturing, and we don’t have to be afraid our current framework will become obsolete tomorrow (looking at you, Meteor). But still - What's the point of maintaining a design system when you can't reuse it in a different application?

Solutions: A drama in four acts

So we required a more solid solution to create and showcase interface elements while avoiding dealing with framework-specific shenanigans at that time. Moving the components into their own repository or at least a dedicated package in a mono-repository was a good first step to separate concerns, but it did not help us to completely loosen those ties. So we were looking deeper.

Approach #1: Mocking with sinon.js

Coming from a test-driven-development context, simply mocking the packages makes sense at first glance. We already used Jest for unit testing, but unfortunately it has its own way of injecting mocks that can't be reused in Storybook. So we tried to achieve the same thing with sinon.js.

We had to maintain local files that would do nothing more than import and re-export dependencies that need to be mocked:

Now we were able to require these and swap them out at runtime:

Code

This did work in theory, but in practice, it just added too many indirections and complexities to the workflow. If you already have to put the whole application in your head, there is a good chance "correct application of the mocking guidelines" has to drop out of it first. We also played with proxyquire, which should work transparently, but opened a completely new can of worms on the webpack end of things.

Neither approach solved the "framework buy-in" problem either. Each component is still bound to a React framework. We were just able to mock them properly for testing.

Approach #2: Hook based dependency injection

Another idea that came up was injecting these dependencies using a React context. We would put them into a context at the top level of the page and use a unified API of hooks to access them instead of importing them directly.

Code

Using ESLint rules, we were able to simply stop anybody from importing framework dependencies directly, so the mental overhead was manageable. But this meant that we could essentially never write a simple component anymore. Almost everything would invoke one of these hooks. We never really measured, and the performance impacts might be neglectable, but it didn't feel right to access a context from every tip of the document tree, and maintaining the context providers for each project was a huge overhead.

Approach #3: Build-time dependency injection

That leads us directly to the impressive react-magnetic-di. A babel preprocessor that will achieve dependency injection at build time, so it doesn't affect runtime. And it even comes with ESLint plugins to make sure it's always used correctly! Marvellous! The only thing to do is make it work with three different babel configurations in two versions of webpack.

If that had solved all our woes we probably would have decided to bite the bullet and finally learn how to configure webpack. But today is not that day! It turns out that, even when you properly separate your UI code from the framework packages, you tend to mix an awful lot of implicit knowledge into it that doesn't belong there.

Code

Or how would you explain why the url goes into the to and not the href property? Back to the drawing board!

Finally: Property based dependency injection

So the last problem we faced was not about the technical problems of dependency injection, but rather the semantic separation of concerns. The design system should control how this link looks, but the application dictates where it leads to and what's the implementation of the invoked action. If we follow that thread and draw the boundary around these responsibilities rather than package imports, we suddenly end up with a different interface for our component.

Instead of requiring a URL as a primitive string, we declare that we want a whole Link component that already knows how to behave – we only have to apply styling to it.

Code

Fast forward to the Gatsby application code: we fetch data and fill it into our component. Instead of passing the raw URL that was stored somewhere in our CMS, we prepare a React component that already encapsulates all of the linking behaviour.

Code

Since we are going to use this a lot, we should extract the creation of this component into a function.

Code

Now that we have a signature, we can just add a dedicated implementation that we use in Storybook. This time it will use a regular anchor tag, but adds an event handler that logs the link destination to the actions tab instead of actually going there.

Code

Magically, we not only separated our components visuals from the framework that renders it, we also introduced a type-safe interface that clearly assigns each side's responsibilities – and is easy to use as well! This pattern can be easily implemented for other framework components like images and other completely different frameworks. At this point you probably can guess where we’re going.

We published a package that starts to do exactly that – the react-framework-bridge. It's still a work in progress, but it provides central type definitions for links, images and rich text components and implements builders for Gatsby and Storybook.

Next up

If you have  some knowledge about the way React detects when components need to be re-rendered, you might have questions now. Our current solution will re-render MyComponent, even if the values for url and text don't change. This is the case because buildLink will always return a "new" value. We could build memoization into it, so it reuses the generated component for each distinct value of href. But useMemo comes at its own cost, and using it in a function that is called with that many distinct arguments, might do more harm than good. 

Instead, a sane strategy for optimizing re-renders should be built into your higher level component architecture, which we will look at in my next blog post.

Though it was quite the journey, by solving these problems that we regularly faced with React framework dependencies, we were able to improve our workflow and more efficiently iterate on prototypes and ideas during front-end and UX development. Still got questions? Still wondering what’s next for your website? We can help you figure that out. Talk to one of Web Development experts today!