Common Improvements to your Next.js, Tailwind and Storybook Stack
Tags: nextjs, tailwindcss, storybook
Published: 2023-09-01 21:02:08

In the previous article of this series, we saw how to create a Next.js app with Tailwind and Storybook on a Stackomate dev-container. Now, we will see some common patterns and improvements that can be used to make your development workflow easier.

## Avoid Import Path Issues

⚠️ It seems that using the default `@` alias for `Next.js` may create some problems when importing such files in the Storybook server. Replacing them with relative paths, such as `..`, seem to have fixed the issues. We may revisit this issue in the future for further investigation.

## Add Support for SVG Imports
One of the quickest ways to add support for SVG imports is to use the `svgr` cli. The cli will transpile SVG images into React components which can be used directly in your code.

- Create a folder in the `src/` directory of your Next.js project called `svgr`;
- Move all of your SVG images to the `src/svgr` folder;
- Optionally install the `@svgr/cli` package as a `devDependency`: 

```bash
npm install --save-dev @svgr/cli
```

- Run `npx -y @svgr/cli --out-dir src/svgr -- src/svgr`; 

ℹ️ You should re-run this command whenever a new SVG file is added to the `src/svgr` folder.

ℹ️ You may also create a npm script in the `package.json` file to automate calling the command above, such as: 
```javascript
{
    "scripts": {
        "svgr": "npx -y @svgr/cli --out-dir src/svgr -- src/svgr"
    }
}
```
- Import the generated javascript files from the `src/svgr` folder into your project:
```typescript
import MyCustomIcon from '../svgr/my-custom-icon'
```

## Use the `clsx` Library (Recommended)
The `clsx` library facilitates the construction of conditional classes, by receiving strings, objects and arrays. Here are some examples taken from its [documentation](https://www.npmjs.com/package/clsx):

```typescript
import clsx from 'clsx';

clsx('foo', true && 'bar', 'baz');
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
clsx(['foo', 0, false, 'bar']);
```

To install it, run `npm install --save clsx` in your application root folder.

## Use the `twMerge` Library (Optional)

The `twMerge` (`tailwind-merge`) library "merges Tailwind CSS classes without style conflicts" [source](https://www.npmjs.com/package/tailwind-merge). Here is an example taken from its [documentation](https://www.npmjs.com/package/tailwind-merge):

```typescript
import { twMerge } from 'tailwind-merge'

twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
// → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
```

To install it, run `npm install --save tailwind-merge` in your application root folder. The maintainers of `twMerge` have also made a [guide](https://github.com/dcastil/tailwind-merge/blob/v1.14.0/docs/when-and-how-to-use-it.md) explaining about when and how to use it.

## Manage Fonts with `@next/fonts`
Using `@next/fonts` includes benefits such as latency reduction and enhanced privacy. It is also built similarly to the component architecture used by React.
- [Install Google Fonts with Next.js](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts#google-fonts);
- Use a [shared file](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts#reusing-fonts) to host fonts; 
- [How to use Next Fonts in Storybook](https://www.npmjs.com/package/@storybook/nextjs#nextjs-font-optimization).

**Our Recommendation:**
- Create a `src/fonts` folder and an `index.tsx` file, and export all fonts in such file.

```typescript
// src/fonts/index.tsx

import { Rubik_Iso } from 'next/font/google'
import { Inter } from 'next/font/google'

export const rubikIso = Rubik_Iso({ subsets: ['latin'], weight: "400" });
export const inter = Inter({subsets: ['latin']})
```

- Consume the exported fonts in your components
```typescript
// src/stories/Header.tsx

import { rubikIso } from '../fonts';

// ...

export const Header = () => (
  <header>
    <div className={clsx(`bg-red-500 dark:bg-yellow-600`, rubikIso.className)}>
    // ...
```

## Create a `SharedDefaults` Component

Creating a `SharedDefaults` component will provide a common component to be used both by Next.js and Storybook servers. This is a good strategy to declare fonts and themes, for example, as they can be referenced in your `App` component and stories.

- Create a `src/components` folder;
- Create a `SharedDefaults.tsx` file which accepts `children` props;

ℹ️ The `SharedDefaults` component may use Tailwind classes and `@next/fonts`. For example:

```typescript
// src/components/SharedDefaults.tsx

import { rubikIso } from "../fonts";
import clsx from "clsx";
import { ReactNode } from "react";

export const SharedDefaults = ({children}: {children: ReactNode}) => {
    return <div className={clsx("bg-purple-800 text-white", rubikIso.className)}>
        {children}
    </div>
}
```

- Wrap your Next.js components with the `SharedDefaults` component.

```typescript
// src/app/page.tsx

import { SharedDefaults } from '@/components/SharedDefaults'

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <SharedDefaults>
        <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        // ...
        </div>
      </SharedDefaults>
    </main>
  )
}
```
- Wrap Storybook story components with `SharedDefaults`.

ℹ️ Instead of wrapping each `stories` component files in a `SharedDefaults` component, we recommend declaring the latter as a **[Storybook Decorator](https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking)** for **context-mocking** in the `.storybook/preview.tsx` file:
- Stop the storybook server;
- Rename the `.storybook/preview.ts` file to use `.tsx` extension;
- Append the `SharedDefaults` wrapper to the `decorators` property (and declare a `React` import explicitly):
```typescript
// .storybook/preview.tsx

import React from 'react';
import { SharedDefaults } from '../src/components/SharedDefaults';

// ...

  decorators: [
    // ... ,
    (Story) => (
      <SharedDefaults>
        <Story />
      </SharedDefaults>
    ),
  ]
```
ℹ️ If you don't see the `SharedDefaults` fonts/colors being applied in Storybook, it may be because the specific component overrides the font in its classes. For example, the `Header.tsx` component contains a `storybook-header` class that uses the `Nunito Sans` font. To verify that `SharedDefaults` fonts and themes are being applied, remove the conflicting classes from the `className` in the targeted components, and try again.

## Conclusion

Although Next.js, Tailwind and Storybook have come a long way regarding mutual integration, some of the challenges mentioned in this article can arise for the developers. By applying our suggestions, it is possible to reduce the amount of work required in future phases of development, while still maintaining a consistent workflow.