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?

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.

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!

5 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?

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

1 Like

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

Thanks so much for the fix @cristianoc