twin.macro: Using Next 13's app directory with `withTwin` setup causing `"use client"` to be removed

I’m currently migrating a Next 13 project from the /pages directory setup to the new /app directory. As part of that it defaults to components being server components, and you must specify "use client" at the top of a file to make it a client side component.

As @ben-rogerson helpfully pointed out (🙏), there’s a guide on how to use styled-components with this new setup, requiring a /lib/registry.ts file.

I got that working in another project (using the /pages directory), but when using with the /app setup the initial "use client" line is stripped out by the build process.

To try to get to the bottom of it I’ve created a fresh next 13 install using yarn create next-app --typescript as detailed here.

Then I went about following the guidance of integrating withTwin to allow for both SWC and webpack to run side by side, as shown here.

Unfortunately I’m still seeing the build process remove the "use client" line, producing this error:

./src/lib/registry.tsx
ReactServerComponentsError:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.

   ,-[/Users/fredrivett/code/FR/next-13-use-client-issue/src/lib/registry.tsx:1:1]
 1 | import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
 2 | import React, { useState } from "react";
   :                 ^^^^^^^^
 3 | import { useServerInsertedHTML } from "next/navigation";
 4 | import { ServerStyleSheet, StyleSheetManager } from "styled-components";
 5 | export default function StyledComponentsRegistry({ children  }) {
   `----

Maybe one of these should be marked as a client entry with "use client":
  src/lib/registry.tsx
  src/app/layout.tsx

I’m unsure how to get around this, as this is quite a minimal setup. I’m sure it’s a simple config issue but I’m unsure which setting to tweak. With this being reproduced in a pretty vanilla project I thought this might also trip up others, and so an issue here benefit them too.

👉 The project reproducing this issue can be found here: https://github.com/fredrivett/next-13-use-client-issue

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 2
  • Comments: 22 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Hey Fred

Without having quite gotten to the bottom of this, I thought I’d share my findings anyway as someone may be able to help pick this up and find a full solution.

I found there’s a setting in the withTwin.js file that’s causing the error.

// withTwin.js
config.module.rules.push({
        test: /\.(tsx|ts)$/,
        include: includedDirs,
        use: [
          options.defaultLoaders.babel, // < Commenting this line out removes the error
          // ...
        ],
      });

In the defaultLoaders the hasServerComponents option is causing the error:

{
    loader: 'next-swc-loader',
    options: {
      hasServerComponents: true, // < This set as `true` causes the error,
      // ...
    }
}

Right now I’m unsure why hasServerComponents: true causes the use client; directive to be stripped, but I was able to patch the loader like this:

const path = require("path");

// The folders containing files importing twin.macro
const includedDirs = [path.resolve(__dirname, "src")];

module.exports = function withTwin(nextConfig) {
  return {
    ...nextConfig,
    webpack(config, options) {
      const { dev, isServer } = options;
      config.module = config.module || {};
      config.module.rules = config.module.rules || [];

      // Make the loader work with the new app directory
      // https://github.com/ben-rogerson/twin.macro/issues/788
      const patchedDefaultLoaders = options.defaultLoaders.babel;
      patchedDefaultLoaders.options.hasServerComponents = false;

      config.module.rules.push({
        test: /\.(tsx|ts)$/,
        include: includedDirs,
        use: [
          patchedDefaultLoaders,
          {
            loader: "babel-loader",
            options: {
              sourceMaps: dev,
              plugins: [
                require.resolve("babel-plugin-macros"),
                [
                  require.resolve("babel-plugin-styled-components"),
                  { ssr: true, displayName: true },
                ],
                [
                  require.resolve("@babel/plugin-syntax-typescript"),
                  { isTSX: true },
                ],
              ],
            },
          },
        ],
      });

      if (!isServer) {
        config.resolve.fallback = {
          ...(config.resolve.fallback || {}),
          fs: false,
          module: false,
          path: false,
          os: false,
          crypto: false,
        };
      }

      if (typeof nextConfig.webpack === "function") {
        return nextConfig.webpack(config, options);
      }
      return config;
    },
  };
};

I’m not sure of the implications of this but the repo you posted now builds and can be served.

No worries, yeah keep us in the loop on this if you can. I’m keen to keep this open as the app directory feature looks to be where next is heading. 🤞 the patch is a good fix - it may be possible to even remove options.defaultLoaders.babel from the array altogether without issues.

I believe I’ve fixed the error for Next.js 14. Furthermore, twin.macro now works with server components!

Note that I am using ESM. My withTwin.mjs:

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import babelPluginTypescript from "@babel/plugin-syntax-typescript";
import babelPluginMacros from "babel-plugin-macros";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import babelPluginTwin from "babel-plugin-twin";
import * as path from "path";
import * as url from "url";

const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

// The folders containing files importing twin.macro
const includedDirs = [path.resolve(__dirname, "src")];

/** @returns {import('next').NextConfig} */
export default function withTwin(
  /** @type {import('next').NextConfig} */
  nextConfig,
) {
  return {
    ...nextConfig,
    compiler: {
      ...nextConfig.compiler,
      styledComponents: true,
    },
    webpack(
      /** @type {import('webpack').Configuration} */
      config,
      options,
    ) {
      const { dev } = options;
      config.module = config.module || {};
      config.module.rules = config.module.rules || [];

      config.module.rules.push({
        test: /\.(tsx|ts)$/,
        include: includedDirs,
        use: [
          {
            loader: "babel-loader",
            options: {
              sourceMaps: dev,
              plugins: [
                babelPluginTwin,
                babelPluginMacros,
                // no more need for babel-plugin-styled-components
                // see: https://nextjs.org/docs/architecture/nextjs-compiler#styled-components
                [babelPluginTypescript, { isTSX: true }],
              ],
            },
          },
        ],
      });

      if (typeof nextConfig.webpack === "function") {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
        return nextConfig.webpack(config, options);
      }
      return config;
    },
  };
}

@ben-rogerson Any ideas to work on nextjs 14?

// Make the loader work with the new app directory
// https://github.com/ben-rogerson/twin.macro/issues/788
const patchedDefaultLoaders = options.defaultLoaders.babel;
patchedDefaultLoaders.options.hasServerComponents = false;
// TODO(igm): can't use react refresh
patchedDefaultLoaders.options.hasReactRefresh = false;

Patched using the above. Not sure what hasReactRefresh does, but it seems hot reload still works.

This is ace, thanks so much for investigating this and your work in general here @ben-rogerson, it’s much appreciated.

I can confirm that fix also works in my actual project where I first bumped into this issue.

It’s an odd one, I’ve had a quick search myself and couldn’t see anything else mentioning this or anything similar, so may be one that’s best to be left open until the root explanation is found? Up to you.

If anything strange comes up due to this I’ll report back.

Thanks again.

@macalinao Huge thanks for your work with the improved/fixed config 🎉 . I’ve verified it’s working too and I’ve updated the following next examples with the improvements:

styled-components / styled-components (ts) / emotion / emotion (ts) / stitches (ts)

To add to the notes above:

  • In a non-typescript styled-components project you’ll still need babel-plugin-styled-components for jsx syntax support.
  • I’ve switched to .mjs for the next.config and withTwin files only to avoid the require imports. You’re still able to use .js versions of these files, just switch back to the require imports.
  • I’ve added babel-plugin-twin in next.config.mjs but kept it commented out - this should make it easier to setup if needed.

Closing this thread as I think we’ve finally found a good solution.

Sorry for re-openning this but none of the examples work with server components. Even your example uses "use client" everywhere.

@rdgr I’ve updated the next-emotion-typescript example to use the app dir + emotion latest. We need to use the jsx pragma at the moment - here are my findings.

I’ve updated the twin examples with the fix mentioned above and moved all of them to use the app directory - no issues so far.

@ben-rogerson I think the next and t3app examples need to be updated also

Sorry for re-openning this but none of the examples work with server components. Even your example uses "use client" everywhere.

Agreed. The example work in “client component” file (with “use client”). But when use tw`` in “server component”, it still got the same error " createContext only works in Client Components. Add the “use client” directive at the top of the file to use it."

Thank you for continuing to research the issue and updating the example project for next.js 14!

However, it seems that errors similar to the above continuously occur in the example project of next.js 14.

I tried to reproduce the error by creating a simple counter page project using useState on the example project(next-emotion-typescript).

If you are interested, please take a look and help those who are experiencing the same error🥹

Thanks for the response. I also faced a similar issue as faced by @rdgr as I degit the next-styled-components template yesterday. It worked fine all along, till I created a layout file for a route group. I had to add “use client” to the top of the page.tsx and layout.tsx files for the error to go away. But it doens’t feel right. Am I supposed to “use client” on each page.tsx and layout.tsx that is has components styled with tw and css?

Thanks for the amazing work on this project though ❤️

@ben-rogerson I degit the next-emotion-typescript sample but I’m having an error on the first run of npm run dev.

image

On the other hand, next-styled-components-typescript works. It seems this is a known limitation from emotion, whose appDir support isn’t ready yet.