Using React server/client components with Rescript (React 18/NextJS 13)

I’ve spend a bit of time playing around with NextJS’s experimental app/ directory setup which introduces React’s new server components (RSC), but it seems like it’s not possible use the new features with Rescript.

How it currently works is that you add a 'use client'; directive at the top of the file in which you define client components. I can’t figure out how/if this can be done in Rescript. Using %%raw() does not seem to work since it appears after the imports even if you use it on line 1. Is there any other way I can get around this?

Also, it seems like colocating pages (always named page.<ext>), and layouts (always named layout.<ext>) with components is the convention that is being advocated for when it comes to the app/ directory in NextJS 13+. Since it’s not possible to have the pages and layouts in Rescript anymore because the naming convention means code duplication, following this would mean mixing .res and .js files in the same directory. I understand that the no-duplicate-filenames is very intentional, but could it make sense to have some kind of escape hatch for this, given that you’re fine with the contents of these files not being possible to import elsewhere?

1 Like

Regardless use client.

You can specify a js script that runs after every file compilation - Build System Configuration | ReScript Language Manual

I made a script postProcessJS.cjs:

const fs = require("fs");
const parser = require("@babel/parser");
const t = require("@babel/types");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;

const [_nodePath, _scriptPath, filePath] = process.argv;
const useClientDirective = "use client";

let file = fs.readFileSync(filePath, "utf-8");

const ast = parser.parse(file, {
  sourceType: "module",
});

let hasUseClient = false;

traverse(ast, {
  StringLiteral: function (path) {
    if (path.node.value === useClientDirective) {
      hasUseClient = true;
      path.remove();
      return;
    }
  },
});

if (hasUseClient) {
  traverse(ast, {
    Program(path) {
      path.unshiftContainer("body", t.stringLiteral(useClientDirective));
    },
  });

  let modified = generate(ast).code;
  fs.writeFileSync(filePath, modified);
}

and included it in my bsconfig:

  "js-post-build": {
    "cmd": "path/to/node ../../postProcessJS.cjs"
  }

What it does - as soon as it spots use client string - it will remove it from the file and add on top of it (before imports);
So %%raw("use client") would work as intended.

1 Like

Regardless file naming.
You can use the same approach with js-post-build - for example, you have CartPage.res, CartPage_Layout.res and CartPage_Error.res - you will create files via fs.writeFileSync and have /cart/page.mjs, /cart/layout.mjs, /cart/error.mjs.

Or you can simply create those js files by hand and import compiled from rescript file.
Something like /cart/page.mjs:

import { make as Component } from 'src/pages/cart/CartPage.mjs;

export default Component;
1 Like

We use this approach at my company, and I think it’s actually the best way of working with Next.js routing as of now.

Also, you can write it a little bit shorter:

export { make as default } from 'src/pages/cart/CartPage.mjs;

If I have time in the next year, I think of writing a small code-gen tool to generate the /pages directory with reexports. It’ll reduce manual labor and make the solution a little bit more typesafe.

1 Like

10.1.3 will feature a new (experimental) way to do this:

@@directive("'use client';")

That will print 'use client'; on the top of the generated file before any imports. Should make it much easier to use server components!

7 Likes

Hi @zth please to see that 10.1.3 is out :slight_smile:

However I’m not seeing the use client directive being added to the compiled JS.
Is there some other setting to add? In bsconfig.json perhaps?

1 Like

I can’t use it either, not with 10.1.3. nor with 11.x and even the example code from the compiler tests does not give me the same results (i.e. it does not generate the first two lines as advertised).

@cristianoc something is missing.

See here: Fix implementation of directives. by cristianoc · Pull Request #6052 · rescript-lang/rescript-compiler · GitHub

2 Likes

ah damn, I guess we’ll have to wait for 10.1.4 :grimacing:

Thanks so much for the fix @cristianoc

could this be worked into decorators? @react.clientComponent @react.serverComponent @react.sharedComponent?

1 Like

Some time passed in the thread, so just to note my best practice here:

In client code, in particular on the threshold when I switch from server component to client component, I use

@@directive("'use client';")

In RSC code I don’t add this or any other directive.

Note that there is a use server; but disturbing as it is, that is not the counterpart of use client, but it has to do with Server Actions which is an entirely different concept (and which I don’t currently use).

I also have the following that I sometimes add to code that I only want to use from RSC:

@@directive("require('server-only');")

This is just a safeguard that makes sure that this code cannot accidentally “become” a client component.

It also has a client-only counterpart. This would error when a module designated for client only would accidentally be used from a RSC. But I have no use case where this would be useful.

That’s it… I’d prefer a more consistent pattern, but the inconsistency originates from nextjs, thus it cannot be resolved from Rescript.

2 Likes

yes, i definitely like this idea better than pure ‘use client’/‘use server’ directives. Especially, since they are implementation dependent. I’ve seen $dollarprefix, i’ve seen dollarsuffix$, i’ve seen ‘use server’, who knows what’s coming next. I strongly feel like ‘use client/server’ would be a mistake for ReScript.

1 Like