Skip to content

'use client' directive for Client Components #4699

Open

Description

@0xcadams

Feature Use Case

Next.js recently released v13 with a beta version of Client Components based on React's RFC 227. They recommend that third-party npm packages use the "use client" directive to indicate when a component is a Client Component, to avoid compilation issues.

Currently, builds using Rollup fail with "Module level directives cause errors when bundled" due to not supporting directives on a module.

Feature Proposal

Allow a 'use client' module-level directive to be preserved in bundling of a library.

Activity

  1. lukastaegert commented on Nov 2, 2022

    @lukastaegert
    Member

    The reason we forbid module level directives is because after bundling, they will end up in a merged chunk with lots of other modules without any module boundaries. So the directive would apply to all other modules as well, which is usually very problematic. If you need such directives in the output, I would recommend to add them via the banner option/plugin hook.

  2. lukastaegert commented on Nov 2, 2022

    @lukastaegert
    Member
  3. 0xcadams commented on Nov 3, 2022

    @0xcadams
    Author

    That makes sense, thanks for the quick response. Okay, we will look at configuration for preconstruct (which is using rollup under the hood).

  4. jaslong commented on Dec 20, 2022

    @jaslong

    The reason we forbid module level directives is because after bundling, they will end up in a merged chunk with lots of other modules without any module boundaries. So the directive would apply to all other modules as well, which is usually very problematic. If you need such directives in the output, I would recommend to add them via the banner option/plugin hook.

    Note if you use the banner option AND terser, terser will strip away your "use client" directive. Currently, one workaround is to disable the compress.directives option, but this is sub-optimal because it could result in duplicate directives. I've started an issue with the terser folks for a proper solution.

    The other alternative is to use a banner plugin like rollup-plugin-banner2. Do NOT use rollup-plugin-banner, because it forces the banner into a comment. Make sure your banner plugin comes AFTER terser.

  5. TkDodo commented on Jan 4, 2023

    @TkDodo

    Saw the warnings as well in our PR today:

    I'm questioning if we really need "use client" to be present in a bundled version of the library? I think rollup ignoring it should be just fine ...

  6. zeilsell-user1 commented on Jan 4, 2023

    @zeilsell-user1

    @jaslong, am I correct in saying that applying rollup-plugin-banner2 with 'use client' then the whole library would be tagged as client side?

  7. mryechkin commented on Mar 9, 2023

    @mryechkin

    @jaslong, am I correct in saying that applying rollup-plugin-banner2 with 'use client' then the whole library would be tagged as client side?

    This unfortunately is also the case with the compress.directives workaround that @jaslong mentioned. Is there even a way around that though?

  8. Ephem commented on Mar 11, 2023

    @Ephem

    For a bundled build, directives like 'use client' is indeed all or nothing which is unfortunate. Is there anything stopping Rollup from preserving these directives when preserveModules: true though?

    If that were the case, as library authors we could say "if you want to use the RSC features, you need to use this output format of the library", while still providing a bundled build for backwards compatibility.

  9. Ephem commented on Mar 11, 2023

    @Ephem

    I did some digging into this to see if it was something I could contribute on. I found where this is happening: https://github.com/rollup/rollup/blob/master/src/ast/nodes/ExpressionStatement.ts

    Being unfamiliar with the code base and being limited on time I ran into two problems though:

    • I could not seem to find how to read the preserveModules option from there. Looks like you could access it from this.context.options.preserveModules, but that seems as if it's always undefined even if set to true in the config.
    • If I hardcoded a if (this.directive === "use client") return true in shouldBeIncluded (just to test), that directive is included below the imports which wont work.

    Just thought I'd document my findings if someone else wants to take a stab at this (if such a contribution is even desirable?).

  10. lukastaegert commented on Mar 11, 2023

    @lukastaegert
    Member

    that directive is included below the imports which won't work

    Yes, imports are removed and completely rewritten in the finalizers. To preserve directives, one would need to pass the directive to the finalizer, which is a bigger refactoring of the Rollup code base.

  11. Ephem commented on Mar 12, 2023

    @Ephem

    Yes, imports are removed and completely rewritten in the finalizers. To preserve directives, one would need to pass the directive to the finalizer, which is a bigger refactoring of the Rollup code base.

    Thanks, that clarified it!

    I gave a shot at implementing this as a plugin instead. It's a somewhat rough v0.1 (but feature complete, I think), there are no tests etc but in my own testing it seems to work well for us in the React Query build at least. Since it's my first Rollup plugin I would appreciate more eyes on it and help trying it out, and hope it helps someone else in this thread!

    https://github.com/Ephem/rollup-plugin-preserve-directives

    Quick version: npm install -D rollup-plugin-preserve-directives

    // rollup.config.js
    import preserveDirectives from "rollup-plugin-preserve-directives";
    
    export default {
      output: {
        // If this is false, plugin does nothing
        preserveModules: true,
      },
      plugins: [preserveDirectives()],
    };
  12. mryechkin commented on Mar 13, 2023

    @mryechkin

    @Ephem you're a legend, thank you! Plugin works perfectly for me. Helped that I was already using preserveModules: true in my config 😄

  13. Ephem commented on Mar 14, 2023

    @Ephem

    @mryechkin Glad to hear its working well! If that changes, please let me know. 😉

  14. 23 remaining items

  15. gmaghame commented on Mar 7, 2024

    @gmaghame

    Is there any configuration needed in a bundler like the one nextjs uses to make it recognize and respect the "use client" directive in a component from an npm package?

    I've created a component library, used Ephem's plugin to preserve the directives, and published it to a private npm registry . However, when I install it and try to use the compoentns directly in my nextjs pages they are being considered server components. If I wrap them in another component that is marked with "use client" they work but I don't want to have to do that.

  16. imtheaman commented on Mar 15, 2024

    @imtheaman

    @gmaghame Hey, please keep us updated if ya find any solution to the problem

  17. huozhi commented on Mar 15, 2024

    @huozhi

    We built a plugin rollup-preserve-directives for preservig directives like "use xxx" and used in a JS bundler, so far we experienced it with few libraries and it works pretty well. This plugin can be an optional solution if you need to keep the original directives in the bundle

  18. hiWaldoDavid commented on May 16, 2024

    @hiWaldoDavid

    I ran into this issue yesterday and spent quite a bit of time searching for a solution. I'll post mine here for anyone else that gets stuck.

    • Install the rollup-preserve-directives package mentioned earlier in this thread.
    • Create a file, rollup.config.js, in the root directory of the library you want to build.
    • Copy and paste the contents of ./node_modules/@nx/react/plugins/bundle-rollup.js into the new file.
    • Add the call to preserveDirectives as the last entry in the plugins list (adding it before will throw an error).
    • Edit options.output to include preserveModules: true. Output can be an array, so it needs to be included for each build format.
    • In the project.json file, point targets.build.options.rollupConfig to the new config file.

    This is what I have:

    Object.defineProperty(exports, "__esModule", { value: true });
    
    function getRollupOptions(options) {
      const defaultGlobals = {
        react: 'React',
        'react-dom': 'ReactDOM',
        'styled-components': 'styled',
        '@emotion/react': 'emotionReact',
        '@emotion/styled': 'emotionStyled',
      };
    
      if (Array.isArray(options.output)) {
        options.output.forEach((output) => {
          output.preserveModules = true;
          output.globals = { ...output.globals, ...defaultGlobals };
        });
      } else {
        const globals = !options.output?.globals
          ? defaultGlobals
          : { ...options.output.globals, ...defaultGlobals };
    
        options.output = {
          ...options.output,
          preserveModules: true,
          globals,
        };
      }
    
      try {
        const url = require('@rollup/plugin-url');
        const svg = require('@svgr/rollup');
        const preserveDirectives = require('rollup-plugin-preserve-directives').preserveDirectives;
    
        options.plugins = [
          svg({
            svgo: false,
            titleProp: true,
            ref: true,
          }),
          url({
            limit: 10000, // 10kB
          }),
          ...(Array.isArray(options.plugins) ? options.plugins : []),
          preserveDirectives(),
        ];
      } catch {
        console.log('Ignored for React Native');
      }
    
      options.onwarn = (warning, warn) => {
        if (warning.code !== 'MODULE_LEVEL_DIRECTIVE') {
          warn(warning);
        }
      };
    
      return options;
    }
    
    module.exports = getRollupOptions;

    I made a few minor changes, but kept it mostly as I found it in the plugin folder.

    This is bundling my library with the 'use client' directives, and without any warnings.

  19. Ben-at-Catalyst commented on Aug 7, 2024

    @Ben-at-Catalyst

    The reason we forbid module level directives is because after bundling, they will end up in a merged chunk with lots of other modules without any module boundaries. So the directive would apply to all other modules as well, which is usually very problematic. If you need such directives in the output, I would recommend to add them via the banner option/plugin hook.

    For those who don't understand what to do here, in your rollup config add this:

    output: {
        banner: "'use client';",
    },
    

    This adds 'use client'; to the top of every bundled .js file.

  20. will-stone commented on Aug 7, 2024

    @will-stone

    @Ben-at-Catalyst What about components you don't wish to add to the client-side bundle, and want them to benefit from server-side rendering? That's a great solution if every component in your library requires client-side JS to operate, but otherwise will unnecessarily bloat the JS bundle.

  21. Ben-at-Catalyst commented on Aug 7, 2024

    @Ben-at-Catalyst

    @will-stone I believe in that case you can add "use server" to the top of induvidual components instead of at the module level. My problem is I have an index.ts barrel file with "use client" at the top because i want the whole package to just be client rendered, this was breaking with rollup.

  22. mryechkin commented on Aug 8, 2024

    @mryechkin

    @Ben-at-Catalyst What about components you don't wish to add to the client-side bundle, and want them to benefit from server-side rendering? That's a great solution if every component in your library requires client-side JS to operate, but otherwise will unnecessarily bloat the JS bundle.

    You could use the rollup-plugin-preserve-directives plugin to selectively add "use client" only to the components that need it.

    I wrote about this on my blog here for reference: https://www.misha.wtf/blog/rollup-server-components

    EDIT: Didn't realize it was actually discussed earlier in this thread, see this comment above.

  23. Ben-at-Catalyst commented on Aug 8, 2024

    @Ben-at-Catalyst

    @Ben-at-Catalyst What about components you don't wish to add to the client-side bundle, and want them to benefit from server-side rendering? That's a great solution if every component in your library requires client-side JS to operate, but otherwise will unnecessarily bloat the JS bundle.

    You could use the rollup-plugin-preserve-directives plugin to selectively add "use client" only to the components that need it.

    I wrote about this on my blog here for reference: https://www.misha.wtf/blog/rollup-server-components

    EDIT: Didn't realize it was actually discussed earlier in this thread, see this comment above.

    I saw this plugin, but in my case I don't really care about the benefits of SSR and just need client components. I just thought with my original comment I'd try and explain the simplest action someone can take to resolve the error messages I was getting before finding this thread.

  24. szhsin commented on Feb 8, 2025

    @szhsin

    The reason we forbid module level directives is because after bundling, they will end up in a merged chunk with lots of other modules without any module boundaries. So the directive would apply to all other modules as well, which is usually very problematic. If you need such directives in the output, I would recommend to add them via the banner option/plugin hook.

    For anyone looking for a plugin to selectively add 'use client' based on glob pattern matching the module name, here's a simple one I created.
    https://github.com/szhsin/rollup-plugin-add-directive

    Hope it helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions