ben-rogerson / twin.macro

πŸ¦Ήβ€β™‚οΈ Twin blends the magic of Tailwind with the flexibility of css-in-js (emotion, styled-components, solid-styled-components, stitches and goober) at build time.
MIT License
7.92k stars 183 forks source link

Using Next 13's app directory with `withTwin` setup causing `"use client"` to be removed #788

Closed fredrivett closed 8 months ago

fredrivett commented 1 year ago

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

ben-rogerson commented 1 year ago

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.

fredrivett commented 1 year ago

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.

ben-rogerson commented 1 year ago

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.

macalinao commented 1 year ago

I'm still getting this error:

./src/lib/registry.tsx
ReactServerComponentsError:

The "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.

   ,-[/<redacted>/src/lib/registry.tsx:1:1]
 1 | import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
 2 | var _s = $RefreshSig$();
 3 | "use client";
   : ^^^^^^^^^^^^^
 4 | import React, { useState } from "react";
 5 | import { useServerInsertedHTML } from "next/navigation";
 6 | import { ServerStyleSheet, StyleSheetManager } from "styled-components";
   `----

Import path:
  ./src/lib/registry.tsx

Any ideas? Thanks!

macalinao commented 1 year ago
// 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.

dzcpy commented 1 year ago

So is there any solution yet?

chrisivo commented 1 year ago
```ts
// 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.

Have tried all these - server-side of the CSS now working.

However, whenever I use the tw attribute, I'm getting the following error in the browser:

Warning: Prop `className` did not match. Server: "Navigation___StyledDiv-sc-f28daec0-0 VwAlI" Client: "Navigation___StyledDiv-sc-18d85156-0 jdQjVx"
    at div
    at O (webpack-internal:///(app-client)/./node_modules/styled-components/dist/styled-components.browser.esm.js:33:23409)
    at header
    at Navigation
    at StyledComponentsRegistry (webpack-internal:///(app-client)/./src/lib/registry.tsx:18:11)
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:70:9)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:51:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:59:11)
    at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:318:11)
    at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:150:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:77:9)
    at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:395:13)
    at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:166:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:182:11)
window.console.error @ app-index.js:31
console.error @ hydration-error-info.js:45
overrideMethod @ console.js:213
printWarning @ react-dom.development.js:94
error @ react-dom.development.js:68
warnForPropDifference @ react-dom.development.js:31222
hydrateAttribute @ react-dom.development.js:32674
diffHydratedGenericElement @ react-dom.development.js:33074
diffHydratedProperties @ react-dom.development.js:33454
hydrateInstance @ react-dom.development.js:34448
prepareToHydrateHostInstance @ react-dom.development.js:6974
completeWork @ react-dom.development.js:18655
completeUnitOfWork @ react-dom.development.js:24746
performUnitOfWork @ react-dom.development.js:24551
workLoopConcurrent @ react-dom.development.js:24526
renderRootConcurrent @ react-dom.development.js:24482
performConcurrentWorkOnRoot @ react-dom.development.js:23339
workLoop @ scheduler.development.js:261
flushWork @ scheduler.development.js:230
performWorkUntilDeadline @ scheduler.development.js:537
Show 1 more frame
Show less

Seems like App Router is still giving me issues with all the functionality that first attracted me towards NextJS...

rdgr commented 1 year ago

Will keep an eye on this thread. Panda CSS is getting some attention by fully supporting the appDir but I still prefer the DX from Twin. Keep up the great work!

ben-rogerson commented 1 year ago

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.

rdgr commented 1 year ago

@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.

ben-rogerson commented 1 year ago

Looks like I'll have to roll that example back as I clearly didn't test it properly. I'm also having trouble avoiding the same errors and in the process of trying different workarounds.

reedwane commented 1 year ago

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 commented 1 year ago

@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.

mthmcalixto commented 11 months ago

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

jay9884 commented 9 months ago

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πŸ₯Ή

macalinao commented 8 months ago

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;
    },
  };
}
jay9884 commented 8 months ago

@macalinao

Thank you very much for your reply πŸ˜ƒ I can solve the problem I was experiencing by applying the withTwin.mjs file above.

Here's some information for those who want to apply this file:

  1. If you are using emotion not styled components, just delete styledComponents: true. Then you can work without installing styled components.

  2. There is no babel-plugin-twin in the package.json of the example project(next-emotion-typescript) now, but installation is required to use the withTwin.mjs file above.

Thank you again for providing the answer to a problem I have been pondering for several days.

ben-rogerson commented 8 months ago

@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:

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

ciokan commented 8 months ago

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

tanomsakk commented 7 months ago

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."

JGJP commented 7 months ago

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

0n3byt3 commented 6 months ago

@ciokan Same here... doesn't work with next js 14 app directory & server components... seems like it's styled-components problem(css in js); this is what next js doc says

Warning: CSS-in-JS libraries which require runtime JavaScript are not currently supported in Server Components. Using CSS-in-JS with newer React features like Server Components and Streaming requires library authors to support the latest version of React, including concurrent rendering We're working with the React team on upstream APIs to handle CSS and JavaScript assets with support for React Server Components and streaming architecture.

@ben-rogerson i think it's better this be mentioned somewhere in nextjs-styled-components tuts

njfix6 commented 2 months ago

Any update on this? I am also running into this issue with nextjs 14.