Skip to content

Commit fc2d79d

Browse files
justin808claude
andcommitted
Fix React 19 server bundle errors by removing 'use client' from RSCProvider and RSCRoute
Remove 'use client' directives from RSCProvider.tsx and RSCRoute.tsx to fix webpack compilation errors when these files are included in server bundles. In React 19, the server build doesn't export client-only APIs like createContext, useContext, and Component. When files with 'use client' are bundled into server bundles, webpack fails with "export not found" errors. The solution is to remove 'use client' from library files that need to work in both server and client contexts. The 'use client' boundary should be established in consuming components, not in the library files themselves. Also update import style to use ReactClient from 'react/index.js' for better compatibility with React 19's dual package structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 17cde51 commit fc2d79d

File tree

6 files changed

+152
-22
lines changed

6 files changed

+152
-22
lines changed

.test-files/should-fail.server.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client';
2+
3+
import React from 'react';
4+
5+
export function TestComponent() {
6+
return <div>This should fail linting</div>;
7+
}

eslint-rules/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Custom ESLint Rules
2+
3+
This directory contains custom ESLint rules specific to React on Rails.
4+
5+
## Rules
6+
7+
### `no-use-client-in-server-files`
8+
9+
Prevents the `'use client'` directive from being used in `.server.tsx` and `.server.ts` files.
10+
11+
#### Why This Rule Exists
12+
13+
Files ending with `.server.tsx` are intended for server-side rendering in React Server Components (RSC) architecture. The `'use client'` directive forces webpack to bundle these files as client components, which creates a fundamental contradiction and causes errors when using React's `react-server` conditional exports.
14+
15+
This issue became apparent with Shakapacker 9.3.0+, which properly honors `resolve.conditionNames` in webpack configurations. When webpack resolves imports with the `react-server` condition, React's server exports intentionally omit client-only APIs like:
16+
17+
- `createContext`, `useContext`
18+
- `useState`, `useEffect`, `useLayoutEffect`, `useReducer`
19+
- `Component`, `PureComponent`
20+
- Other hooks (`use*` functions)
21+
22+
#### Examples
23+
24+
**Incorrect** - Will trigger an error:
25+
26+
```typescript
27+
// Component.server.tsx
28+
'use client';
29+
30+
import React from 'react';
31+
32+
export function MyComponent() {
33+
return <div>Component</div>;
34+
}
35+
```
36+
37+
**Correct** - No directive in server files:
38+
39+
```typescript
40+
// Component.server.tsx
41+
import React from 'react';
42+
43+
export function MyComponent() {
44+
return <div>Component</div>;
45+
}
46+
```
47+
48+
**Correct** - Use `'use client'` in client files:
49+
50+
```typescript
51+
// Component.client.tsx or Component.tsx
52+
'use client';
53+
54+
import React, { useState } from 'react';
55+
56+
export function MyComponent() {
57+
const [count, setCount] = useState(0);
58+
return <div>Count: {count}</div>;
59+
}
60+
```
61+
62+
#### Auto-fix
63+
64+
This rule includes an automatic fixer that will remove the `'use client'` directive from `.server.tsx` files when you run ESLint with the `--fix` option:
65+
66+
```bash
67+
npx eslint --fix path/to/file.server.tsx
68+
```
69+
70+
#### Related
71+
72+
- **Issue:** [Shakapacker #805 - Breaking change in 9.3.0](https://github.com/shakacode/shakapacker/issues/805)
73+
- **Fix PR:** [React on Rails #1896](https://github.com/shakacode/react_on_rails/pull/1896)
74+
- **Commit:** [86979dca - Remove 'use client' from .server.tsx files](https://github.com/shakacode/react_on_rails/commit/86979dca)
75+
76+
#### Configuration
77+
78+
This rule is automatically enabled in the React on Rails ESLint configuration at the `error` level. It's defined in `eslint.config.ts`:
79+
80+
```typescript
81+
plugins: {
82+
'react-on-rails': {
83+
rules: {
84+
'no-use-client-in-server-files': noUseClientInServerFiles,
85+
},
86+
},
87+
},
88+
89+
rules: {
90+
'react-on-rails/no-use-client-in-server-files': 'error',
91+
// ... other rules
92+
}
93+
```
94+
95+
## Testing
96+
97+
To run tests for the custom rules:
98+
99+
```bash
100+
node eslint-rules/no-use-client-in-server-files.test.cjs
101+
```
102+
103+
## Adding New Custom Rules
104+
105+
To add a new custom ESLint rule:
106+
107+
1. Create the rule file in this directory (use `.cjs` extension for CommonJS)
108+
2. Create a corresponding test file (e.g., `rule-name.test.cjs`)
109+
3. Import and register the rule in `eslint.config.ts`
110+
4. Add documentation to this README

eslint.config.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,18 @@ const config = tsEslint.config([
8585
},
8686
},
8787

88+
plugins: {
89+
'react-on-rails': {
90+
rules: {
91+
'no-use-client-in-server-files': noUseClientInServerFiles,
92+
},
93+
},
94+
},
95+
8896
rules: {
97+
// Custom React on Rails rules
98+
'react-on-rails/no-use-client-in-server-files': 'error',
99+
89100
'no-shadow': 'off',
90101
'no-console': 'off',
91102
'function-paren-newline': 'off',
@@ -168,13 +179,6 @@ const config = tsEslint.config([
168179
},
169180
{
170181
files: ['**/*.server.ts', '**/*.server.tsx'],
171-
plugins: {
172-
'react-on-rails': {
173-
rules: {
174-
'no-use-client-in-server-files': noUseClientInServerFiles,
175-
},
176-
},
177-
},
178182
rules: {
179183
'react-on-rails/no-use-client-in-server-files': 'error',
180184
},

packages/react-on-rails-pro/src/RSCProvider.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212
* https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
1313
*/
1414

15-
'use client';
16-
17-
import * as React from 'react';
15+
import type { ReactNode } from 'react';
16+
import ReactClient from 'react/index.js';
1817
import type { ClientGetReactServerComponentProps } from './getReactServerComponent.client.ts';
1918
import { createRSCPayloadKey } from './utils.ts';
2019

20+
const React = ReactClient as typeof import('react');
21+
2122
type RSCContextType = {
22-
getComponent: (componentName: string, componentProps: unknown) => Promise<React.ReactNode>;
23+
getComponent: (componentName: string, componentProps: unknown) => Promise<ReactNode>;
2324

24-
refetchComponent: (componentName: string, componentProps: unknown) => Promise<React.ReactNode>;
25+
refetchComponent: (componentName: string, componentProps: unknown) => Promise<ReactNode>;
2526
};
2627

2728
const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
@@ -48,9 +49,9 @@ const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
4849
export const createRSCProvider = ({
4950
getServerComponent,
5051
}: {
51-
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<React.ReactNode>;
52+
getServerComponent: (props: ClientGetReactServerComponentProps) => Promise<ReactNode>;
5253
}) => {
53-
const fetchRSCPromises: Record<string, Promise<React.ReactNode>> = {};
54+
const fetchRSCPromises: Record<string, Promise<ReactNode>> = {};
5455

5556
const getComponent = (componentName: string, componentProps: unknown) => {
5657
const key = createRSCPayloadKey(componentName, componentProps);
@@ -76,7 +77,7 @@ export const createRSCProvider = ({
7677

7778
const contextValue = { getComponent, refetchComponent };
7879

79-
return ({ children }: { children: React.ReactNode }) => {
80+
return ({ children }: { children: ReactNode }) => {
8081
return <RSCContext.Provider value={contextValue}>{children}</RSCContext.Provider>;
8182
};
8283
};

packages/react-on-rails-pro/src/RSCRoute.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,22 @@
1414

1515
/// <reference types="react/experimental" />
1616

17-
'use client';
18-
19-
import * as React from 'react';
17+
import type { ReactNode } from 'react';
18+
import ReactClient from 'react/index.js';
2019
import { useRSC } from './RSCProvider.tsx';
2120
import { ServerComponentFetchError } from './ServerComponentFetchError.ts';
2221

22+
const React = ReactClient as typeof import('react');
23+
2324
/**
2425
* Error boundary component for RSCRoute that adds server component name and props to the error
2526
* So, the parent ErrorBoundary can refetch the server component
2627
*/
2728
class RSCRouteErrorBoundary extends React.Component<
28-
{ children: React.ReactNode; componentName: string; componentProps: unknown },
29+
{ children: ReactNode; componentName: string; componentProps: unknown },
2930
{ error: Error | null }
3031
> {
31-
constructor(props: { children: React.ReactNode; componentName: string; componentProps: unknown }) {
32+
constructor(props: { children: ReactNode; componentName: string; componentProps: unknown }) {
3233
super(props);
3334
this.state = { error: null };
3435
}
@@ -75,7 +76,7 @@ export type RSCRouteProps = {
7576
componentProps: unknown;
7677
};
7778

78-
const PromiseWrapper = ({ promise }: { promise: Promise<React.ReactNode> }) => {
79+
const PromiseWrapper = ({ promise }: { promise: Promise<ReactNode> }) => {
7980
// React.use is available in React 18.3+
8081
const promiseResult = React.use(promise);
8182

@@ -88,7 +89,7 @@ const PromiseWrapper = ({ promise }: { promise: Promise<React.ReactNode> }) => {
8889
return promiseResult;
8990
};
9091

91-
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): React.ReactNode => {
92+
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): ReactNode => {
9293
const { getComponent } = useRSC();
9394
const componentPromise = getComponent(componentName, componentProps);
9495
return (
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client';
2+
3+
import React from 'react';
4+
5+
export function TestComponent() {
6+
return <div>This should fail linting</div>;
7+
}

0 commit comments

Comments
 (0)