├── .gitignore ├── index.d.ts ├── .github └── workflows │ └── publish.yml ├── LICENSE.md ├── package.json ├── index.js ├── README.md └── rsc.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as React from "react"; 3 | export = React; 4 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: "The version to publish" 9 | required: true 10 | tag: 11 | description: "Tag" 12 | required: true 13 | default: "latest" 14 | type: choice 15 | options: 16 | - latest 17 | - snapshot 18 | - next 19 | jobs: 20 | publish: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | id-token: write 24 | contents: read 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: "18.x" 30 | registry-url: "https://registry.npmjs.org" 31 | - run: npm pkg set "version=${{ inputs.version }}" 32 | - run: npm publish --provenance --tag ${{ inputs.tag }} --access public 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lenz Weber-Tronic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehackt", 3 | "version": "0.1.0", 4 | "description": "A wrapper around React that will hide hooks from the React Server Component compiler", 5 | "author": "Lenz Weber-Tronic", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/phryneas/rehackt.git" 9 | }, 10 | "homepage": "https://github.com/phryneas/rehackt", 11 | "license": "MIT", 12 | "main": "index.js", 13 | "exports": { 14 | ".": { 15 | "types": "./index.d.ts", 16 | "react-server": "./rsc.js", 17 | "default": "./index.js" 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "files": [ 22 | "index.js", 23 | "index.d.ts", 24 | "rsc.js", 25 | "package.json", 26 | "README.md", 27 | "LICENSE.md" 28 | ], 29 | "peerDependencies": { 30 | "@types/react": "*", 31 | "react": "*" 32 | }, 33 | "peerDependenciesMeta": { 34 | "react": { 35 | "optional": true 36 | }, 37 | "@types/react": { 38 | "optional": true 39 | } 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^20.5.7", 43 | "react": "^19.0.0-canary-33a32441e9-20240418" 44 | }, 45 | "prettier": { 46 | "printWidth": 120 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | if (0) { 3 | // Trick cjs-module-lexer into adding named exports for all React exports. 4 | // (if imported with `import()`, they will appear in `.default` as well.) 5 | // This way, cjs-module-lexer will let all of react's (named) exports through unchanged. 6 | module.exports = require("react"); 7 | } 8 | // We don't want bundlers to error when they encounter usage of any of these exports. 9 | // It's up to the package author to ensure that if they access React internals, 10 | // they do so in a safe way that won't break if React changes how they use these internals. 11 | // (e.g. only access them in development, and only in an optional way that won't 12 | // break if internals are not there or do not have the expected structure) 13 | // @ts-ignore 14 | module.exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = undefined; 15 | // @ts-ignore 16 | module.exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined; 17 | // @ts-ignore 18 | module.exports.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined; 19 | // Here we actually pull in the React library and add everything 20 | // it exports to our own `module.exports`. 21 | // If React suddenly were to add one of the above "polyfilled" exports, 22 | // the React version would overwrite our version, so this should be 23 | // future-proof. 24 | Object.assign(module.exports, require("react")); 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rehackt 2 | 3 | > This package is fairly advanced and is only intended for library developers that want to maintain high interop with Next.js server actions. 4 | 5 | Rehackt invisibly wraps `react` so that you're able to use shared imports with `react` in server-side Next.js code without throwing an error to your users. 6 | 7 | ## Explainer 8 | 9 | Assume you have the following code in a Next.js codebase: 10 | 11 | ```tsx 12 | "use client" 13 | 14 | import { useFormState } from "react-dom" 15 | import someAction from "./action"; 16 | 17 | export const ClientComp = () => { 18 | const [data, action] = useFormState(someAction, "Hello client"); 19 | 20 | return
21 |

{data}

22 | 23 |
24 | } 25 | ``` 26 | 27 | ```tsx 28 | "use server" 29 | // action.ts 30 | 31 | import {data} from "./shared-code"; 32 | 33 | export default async function someAction() { 34 | return "Hello " + data.name; 35 | } 36 | ``` 37 | 38 | ```tsx 39 | // shared-code.ts 40 | import {useState} from "react"; 41 | 42 | export const data = { 43 | useForm: (val: T) => { 44 | useState(val) 45 | }, 46 | name: "server" 47 | } 48 | ``` 49 | 50 | While you're not intending to use `data.useForm` in your `action.ts` server-only file, you'll still receive the following error from Next.js' build process when trying to use this code: 51 | 52 | ```shell 53 | ./src/app/shared-code.ts 54 | ReactServerComponentsError: 55 | 56 | 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. 57 | Learn more: https://nextjs.org/docs/getting-started/react-essentials 58 | 59 | ╭─[/src/app/shared-code.ts:1:1] 60 | 1 │ import {useState} from "react"; 61 | · ──────── 62 | 2 │ 63 | 3 │ export const data = { 64 | 3 │ useForm: (val: T) => { 65 | ╰──── 66 | 67 | Maybe one of these should be marked as a client entry with "use client": 68 | ./src/app/shared-code.ts 69 | ./src/app/action.ts 70 | ``` 71 | 72 | This is because Next.js statically analyzes usage of `useState` to ensure it's not being utilized in server-only code. 73 | 74 | By replacing the import from `react` to `rehackt`: 75 | 76 | ```tsx 77 | // shared-code.ts 78 | import {useState} from "rehackt"; 79 | 80 | export const data = { 81 | useForm: (val: T) => { 82 | useState(val) 83 | }, 84 | name: "server" 85 | } 86 | ``` 87 | 88 | You'll no longer see this error. 89 | 90 | > Keep in mind, this does not enable usage of `useState` in server-only code, this just removes the error described above. 91 | 92 | ## Further Reading 93 | 94 | The following is a list of reading resources that pertain to this package: 95 | 96 | - [My take on the current React & Server Components controversy - Lenz Weber-Tronic](https://phryneas.de/react-server-components-controversy) 97 | 98 | - [apollographql/apollo-client#10974](https://github.com/apollographql/apollo-client/issues/10974) 99 | 100 | - [TanStack/form#480](https://github.com/TanStack/form/issues/480#issuecomment-1793576645) 101 | -------------------------------------------------------------------------------- /rsc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | if (0) { 4 | // Trick cjs-module-lexer into adding named exports for all React exports. 5 | // (if imported with `import()`, they will appear in `.default` as well.) 6 | // This way, cjs-module-lexer will let all of react's (named) exports through unchanged. 7 | module.exports = require("react"); 8 | } 9 | 10 | // missing functions 11 | module.exports.createContext = polyfillMissingFn("createContext"); 12 | // @ts-ignore 13 | module.exports.createFactory = polyfillMissingFn("createFactory"); 14 | module.exports.act = polyfillMissingFn("act"); 15 | // @ts-ignore 16 | module.exports.unstable_act = polyfillMissingFn("unstable_act"); 17 | module.exports.unstable_useCacheRefresh = polyfillMissingFn("unstable_useCacheRefresh"); 18 | module.exports.useContext = polyfillMissingFn("useContext"); 19 | module.exports.useDeferredValue = polyfillMissingFn("useDeferredValue"); 20 | module.exports.useEffect = polyfillMissingFn("useEffect"); 21 | module.exports.useImperativeHandle = polyfillMissingFn("useImperativeHandle"); 22 | module.exports.useInsertionEffect = polyfillMissingFn("useInsertionEffect"); 23 | module.exports.useLayoutEffect = polyfillMissingFn("useLayoutEffect"); 24 | module.exports.useReducer = polyfillMissingFn("useReducer"); 25 | module.exports.useRef = polyfillMissingFn("useRef"); 26 | module.exports.useState = polyfillMissingFn("useState"); 27 | module.exports.useSyncExternalStore = polyfillMissingFn("useSyncExternalStore"); 28 | module.exports.useTransition = polyfillMissingFn("useTransition"); 29 | module.exports.useOptimistic = polyfillMissingFn("useOptimistic"); 30 | // We don't want bundlers to error when they encounter usage of any of these exports. 31 | // It's up to the package author to ensure that if they access React internals, 32 | // they do so in a safe way that won't break if React changes how they use these internals. 33 | // (e.g. only access them in development, and only in an optional way that won't 34 | // break if internals are not there or do not have the expected structure) 35 | // @ts-ignore 36 | module.exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = undefined; 37 | // @ts-ignore 38 | module.exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined; 39 | // @ts-ignore 40 | module.exports.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined; 41 | 42 | // missing classes 43 | module.exports.Component = polyfillMissingClass("Component"); 44 | module.exports.PureComponent = polyfillMissingClass("PureComponent"); 45 | 46 | module.exports.createContext = function unsupportedCreateContext() { 47 | return { 48 | Provider: function throwNoContext() { 49 | throw new Error("Context is not available in this environment."); 50 | }, 51 | Consumer: function throwNoContext() { 52 | throw new Error("Context is not available in this environment."); 53 | }, 54 | }; 55 | }; 56 | // @ts-ignore 57 | module.exports.createFactory = function unsupportedCreateFactory() { 58 | return function throwNoCreateFactory() { 59 | throw new Error("createFactory is not available in this environment."); 60 | }; 61 | }; 62 | 63 | // Here we actually pull in the React library and add everything 64 | // it exports to our own `module.exports`. 65 | // If React suddenly were to add one of the above "polyfilled" exports, 66 | // the React version would overwrite our version, so this should be 67 | // future-proof. 68 | Object.assign(module.exports, require("react")); 69 | 70 | function polyfillMissingFn(exportName) { 71 | const name = "nonExistingExport__" + exportName; 72 | return /** @type {any} */ ( 73 | { 74 | [name]() { 75 | throw new Error(`React functionality '${exportName}' is not available in this environment.`); 76 | }, 77 | }[name] 78 | ); 79 | } 80 | 81 | function polyfillMissingClass(exportName) { 82 | return /** @type {any} */ ( 83 | class NonExistentClass { 84 | constructor() { 85 | throw new Error(`React class '${exportName}' is not available in this environment.`); 86 | } 87 | } 88 | ); 89 | } 90 | --------------------------------------------------------------------------------