Take the Gatsby out of my Storybook
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:
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.
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.
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.
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.
Since we are going to use this a lot, we should extract the creation of this component into a function.
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.
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!