DEV Community

Cover image for How to Use the Next.js Image Component in Storybook
Jonas Schumacher
Jonas Schumacher

Posted on

How to Use the Next.js Image Component in Storybook

In this post, we will be setting up Next.js and Storybook in a way that makes it possible to use Next.js' Image component in components rendered inside Storybook.

Introduction

If you want to take a look at the final product first, or if you don't care about the step-by-step, here's the accompanying repo.

Rendering a component that uses the Next.js Image component inside Storybook might have you running into a few gotchas. I myself ran into two:

  • Storybook can't seem to find the image you statically import from your public directory, resulting in the following error:
Failed to parse src "static/media/public/<imageName>.jpg" on `next/image`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)
Enter fullscreen mode Exit fullscreen mode
  • Storybook can't find the blur placeholder image that is automatically generated by Next.js and injected into the blurDataURL prop, resulting in the following error:
Image with src "static/media/public/<imageName>.jpg" has "placeholder='blur'" property but is missing the "blurDataURL" property.
Enter fullscreen mode Exit fullscreen mode

The reasons for both of these are actually the same: when run inside Storybook, your code won't run through Next.js' build process, during which the correct URLs/Paths and alternative sizes for your hosted images as well as the placeholder images are created and injected into your code.

If you know how, both of these are straightforward to solve. But finding the solutions can be kind of an odyssey. So here they are in one place.

Note: in this post, I assume you have already set up both Next.js as well as Storybook. If not, do that and then come back here.

Storybook Can't Find Images Imported from Next.js' public Directory

Let's assume you have the following component:

// src/components/ImageTest.js

import Image from "next/image"

import testImage from "../public/testImage.jpg"

const ImageTest = () => (
  <Image src={testImage}
    alt="A stack of colorful cans"
    layout="fill" 
  />
)

export default ImageTest
Enter fullscreen mode Exit fullscreen mode

And the following story for it:

// stories/ImageTest.stories.js

import React from 'react';

import ImageTest from '../components/ImageTest';

export default {
  title: 'Image/ImageTest',
  component: ImageTest,
};

const Template = (args) => <ImageTest {...args} />;

export const KitchenSink = Template.bind({});
KitchenSink.args = {};
Enter fullscreen mode Exit fullscreen mode

If you were to run Storybook now, this would be what you saw:

next-storybook-01

When I first encountered this, I reasoned Storybook simply couldn't find the assets in Next.js' public/ directory. But running with the -s public/ command line option to tell it about the directory doesn't solve the problem.

After some digging, the issue here seems to be the things going on behind the scenes of the Image component. One of its most useful features is that it will automatically optimize the image you pass it and create and serve alternative sizings of it on demand. Next.js can't work its magic when the Image component is rendered inside Storybook though, and that's why the solution here is to simply turn these optimizations off in this context. To do this, we'll have to add the following to Storybook's setup code:

// .storybook/preview.js

import * as NextImage from "next/image";

const OriginalNextImage = NextImage.default;

Object.defineProperty(NextImage, "default", {
  configurable: true,
  value: (props) => (
    <OriginalNextImage
      {...props}
      unoptimized
    />
  ),
});
Enter fullscreen mode Exit fullscreen mode

This code will replace the Image component's default export with our own version, which adds the unoptimized prop to every instance of it. With this, "the source image will be served as-is instead of changing quality, size, or format", according to the Next.js documentation. And since we added this to Storybook's setup code, this will only be done when our component is rendered inside Storybook.

Credit for this solution goes to Github user rajansolanki, who synthesized a few prior solution attempts in this comment on the relevant Github issue.

If you want to read more about Next.js' Image component and the props you can pass into it, take a look at the introduction to its features as well as its documentation.

Storybook Can't Find the Blur Placeholder Image That is Automatically Generated by Next.js and Injected Into the blurDataURL Prop

Another nice feature of the Image component is that it will automatically generate small, blurred placeholder images for display during the loading of the full image.

All we need to do to activate this feature is to pass the placeholder="blur" prop:

// src/components/ImageTest.js

import Image from "next/image"

import testImage from "../public/testImage.jpg"

const ImageTest = () => (
  <Image src={testImage}
    alt="A stack of colorful cans"
    layout="fill"
    placeholder="blur" // this is new!
  />
)

export default ImageTest
Enter fullscreen mode Exit fullscreen mode

But this will immediately result in the following error when we run Storybook again:

next-storybook-02

The reason for this is basically the same as before. Next.js will generate a placeholder image and inject it into the component for us. So that one line of code we added actually does a whole lot in the background, all of which, again, is not automatically done when run inside Storybook. Luckily the solution is a one liner as well:

// .storybook/preview.js

import * as NextImage from "next/image";

const OriginalNextImage = NextImage.default;

Object.defineProperty(NextImage, "default", {
  configurable: true,
  value: (props) => (
    <OriginalNextImage
      {...props}
      unoptimized
      // this is new!
      blurDataURL="data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAADAAQDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAbEAADAAMBAQAAAAAAAAAAAAABAgMABAURUf/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAFxEAAwEAAAAAAAAAAAAAAAAAAAECEf/aAAwDAQACEQMRAD8Anz9voy1dCI2mectSE5ioFCqia+KCwJ8HzGMZPqJb1oPEf//Z"
    />
  ),
});
Enter fullscreen mode Exit fullscreen mode

What we have done here is a bit of a hack, granted. But it's effective, as all good hacks are. With this, we're setting all placeholder images to be the same data, at least in the context of Storybook. The string above is actually the base64 representation of the placeholder for the plaiceholder example image on their homepage. But we could just as easily upload our own image there and use that.

And with this, you should see the following when running Storybook:

next-storybook-04

Note: our testImage for this is this image by Studio Blackthorns on Unsplash.

If you want to read more about the automatic placeholder generation of Next.js' Image component, take a look at the placeholder prop's documentation.

Don't forget that you can use or take a look at the accompanying repo.

💡 This post was published 2021/07/21 and last updated 2021/07/21. Since node libraries change and break all the time, there's a non-trivial chance that all of this is obsolete and none of it works when you read this in your flying taxi, gulpin' on insect protein shakes sometime in the distant future or, let's be honest here, next week. If so, tell me in the comments and I'll see if an update would still be relevant.

Top comments (20)

Collapse
 
damsalem profile image
Dani Amsalem

Very, very helpful! I'm also using Sanity.io as a CMS and had to use a fallback prop since I'm not passing in JSON data for each image. If anyone else wants to know what I did, I just used the OR operator like so:

<Image
  src={storyHero || urlFor(heroImage.url).auto("format").size(600, 338).url()}
  alt={storyAlt || heroImage?.alt}
  width={600}
  height={338}
  layout={"responsive"}
/>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
chema profile image
José María CL

Hey! Thank you so much! You've saved me a lot of time

Collapse
 
jonasmerlin profile image
Jonas Schumacher

Glad it helped!

Collapse
 
irveloper profile image
Irving Caamal

Works like a charm, thanks.

Collapse
 
ryankilleen profile image
Ryan Killeen

This was a life-saver, thanks!

Collapse
 
bilalmohib profile image
Muhammad Bilal Mohib-ul-Nabi

Thank you very much for sharing.It was really helpful.

Collapse
 
xiaoyun profile image
Xiao Yun

Very helpful post. I struggled to find solution for 3 hours but no other blog could help me. I solved after reading this post.
Many thanks Jonas!!! :)

Collapse
 
alfredosalzillo profile image
Alfredo Salzillo

It's not working for me

Collapse
 
jonasmerlin profile image
Jonas Schumacher

Something might have changed since I wrote the post. Can you tell me more specifics? What is not working exactly? Do you get any error messages? Then I will try to update the post.

Collapse
 
burdiuz profile image
Oleg Galaburda

Didn't work for me either. What worked for me is providing defaultProps with unoptimized = true in "preview.js"

import Image from 'next/image';

Image.propTypes = {
  unoptimized: null,
};

Image.defaultProps = {
  unoptimized: true,
};
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pmfeo profile image
Pablo

Thank you both, guys! Oleg's tweak was needed as well in to make it work properly ("next": "12.1.6" + "@storybook/react": "^6.5.9")

Thread Thread
 
jonasmerlin profile image
Jonas Schumacher • Edited

Hey! Sorry, I'm currently working with neither next nor storybook anymore and frankly, don't have the time right now to go into this and comprehend what is going on. Still, this post seems to be pretty popular and I'd like it to be at least correct and runnable when people find it. Would you be willing to suggest the necessary changes to my post to make it map to what you had to do? I would then change the necessary parts and mark you as contributors. Would be really cool!

Collapse
 
rpadovanni profile image
Rafael Padovani

Thank you for sharing, Jonas. I ran into this problem and your article helped me a lot!

Collapse
 
jonasmerlin profile image
Jonas Schumacher

Hey Rafael, really glad to hear that! If you run into any other problems related to this, please tell me about them and your solution and I will add them to the post (with attribution to you of course).

Collapse
 
kouliavtsev profile image
kouliavtsev

The -s public/ seems to be deprecated.

Use staticDirs: ['../public'] in main.js instead.

Collapse
 
thamaragerigr profile image
Thamara Gerig • Edited

Very helpful! Thank you!