Skip to content

Commit 680f01b

Browse files
add a bility to refetch and retry fetching of server components
1 parent 6a82137 commit 680f01b

File tree

7 files changed

+117
-7
lines changed

7 files changed

+117
-7
lines changed

node_package/src/RSCProvider.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ type RSCContextType = {
77
getCachedComponent: (componentName: string, componentProps: unknown) => React.ReactNode;
88

99
getComponent: (componentName: string, componentProps: unknown) => Promise<React.ReactNode>;
10+
11+
refetchComponent: (componentName: string, componentProps: unknown) => Promise<React.ReactNode>;
1012
};
1113

1214
const RSCContext = React.createContext<RSCContextType | undefined>(undefined);
@@ -59,7 +61,20 @@ export const createRSCProvider = ({
5961
return promise;
6062
};
6163

62-
const contextValue = { getCachedComponent, getComponent };
64+
const refetchComponent = (componentName: string, componentProps: unknown) => {
65+
const key = createRSCPayloadKey(componentName, componentProps, railsContext);
66+
cachedComponents[key] = undefined;
67+
const promise = getServerComponent({
68+
componentName,
69+
componentProps,
70+
railsContext,
71+
enforceRefetch: true,
72+
});
73+
fetchRSCPromises[key] = promise;
74+
return promise;
75+
};
76+
77+
const contextValue = { getCachedComponent, getComponent, refetchComponent };
6378

6479
return ({ children }: { children: React.ReactNode }) => {
6580
return <RSCContext.Provider value={contextValue}>{children}</RSCContext.Provider>;

node_package/src/RSCRoute.ts renamed to node_package/src/RSCRoute.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
import * as React from 'react';
22
import { useRSC } from './RSCProvider.tsx';
3+
import { ServerComponentFetchError } from './ServerComponentFetchError.ts';
4+
5+
/**
6+
* Error boundary component for RSCRoute that adds server component name and props to the error
7+
* So, the parent ErrorBoundary can refetch the server component
8+
*/
9+
class RSCRouteErrorBoundary extends React.Component<
10+
{ children: React.ReactNode; componentName: string; componentProps: unknown },
11+
{ error: Error | null }
12+
> {
13+
constructor(props: { children: React.ReactNode; componentName: string; componentProps: unknown }) {
14+
super(props);
15+
this.state = { error: null };
16+
}
17+
18+
static getDerivedStateFromError(error: Error) {
19+
return { error };
20+
}
21+
22+
render() {
23+
const { error } = this.state;
24+
const { componentName, componentProps, children } = this.props;
25+
if (error) {
26+
throw new ServerComponentFetchError(error.message, componentName, componentProps, error);
27+
}
28+
29+
return children;
30+
}
31+
}
332

433
/**
534
* Renders a React Server Component inside a React Client Component.
@@ -28,15 +57,23 @@ export type RSCRouteProps = {
2857
componentProps: unknown;
2958
};
3059

31-
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps) => {
60+
const PromiseWrapper = ({ promise }: { promise: Promise<React.ReactNode> }) => {
61+
return React.use(promise);
62+
};
63+
64+
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): React.ReactNode => {
3265
const { getComponent, getCachedComponent } = useRSC();
3366
const cachedComponent = getCachedComponent(componentName, componentProps);
3467
if (cachedComponent) {
3568
return cachedComponent;
3669
}
3770

3871
const componentPromise = getComponent(componentName, componentProps);
39-
return React.use(componentPromise);
72+
return (
73+
<RSCRouteErrorBoundary componentName={componentName} componentProps={componentProps}>
74+
<PromiseWrapper promise={componentPromise} />
75+
</RSCRouteErrorBoundary>
76+
);
4077
};
4178

4279
export default RSCRoute;

node_package/src/RSCRouteError.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Custom error type for when there's an issue fetching or rendering a server component.
3+
* This error includes information about the server component and the original error that occurred.
4+
*/
5+
export class ServerComponentFetchError extends Error {
6+
serverComponentName: string;
7+
8+
serverComponentProps: unknown;
9+
10+
originalError: Error;
11+
12+
constructor(message: string, componentName: string, componentProps: unknown, originalError: Error) {
13+
super(message);
14+
this.name = 'ServerComponentFetchError';
15+
this.serverComponentName = componentName;
16+
this.serverComponentProps = componentProps;
17+
this.originalError = originalError;
18+
}
19+
}
20+
21+
/**
22+
* Type guard to check if an error is a ServerComponentFetchError
23+
*/
24+
export function isServerComponentFetchError(error: unknown): error is ServerComponentFetchError {
25+
return error instanceof ServerComponentFetchError;
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Custom error type for when there's an issue fetching or rendering a server component.
3+
* This error includes information about the server component and the original error that occurred.
4+
*/
5+
export class ServerComponentFetchError extends Error {
6+
serverComponentName: string;
7+
8+
serverComponentProps: unknown;
9+
10+
originalError: Error;
11+
12+
constructor(message: string, componentName: string, componentProps: unknown, originalError: Error) {
13+
super(message);
14+
this.name = 'ServerComponentFetchError';
15+
this.serverComponentName = componentName;
16+
this.serverComponentProps = componentProps;
17+
this.originalError = originalError;
18+
}
19+
}
20+
21+
/**
22+
* Type guard to check if an error is a ServerComponentFetchError
23+
*/
24+
export function isServerComponentFetchError(error: unknown): error is ServerComponentFetchError {
25+
return error instanceof ServerComponentFetchError;
26+
}

node_package/src/getReactServerComponent.client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type ClientGetReactServerComponentProps = {
1414
componentName: string;
1515
componentProps: unknown;
1616
railsContext: RailsContext;
17+
enforceRefetch?: boolean;
1718
};
1819

1920
const createFromFetch = async (fetchPromise: Promise<Response>) => {
@@ -116,6 +117,7 @@ const createFromPreloadedPayloads = (payloads: string[]) => {
116117
* @param componentName - Name of the server component to render
117118
* @param componentProps - Props to pass to the server component
118119
* @param railsContext - Context for the current request
120+
* @param enforceRefetch - Whether to enforce a refetch of the component
119121
* @returns A Promise resolving to the rendered React element
120122
*
121123
* @important This is an internal function. End users should not use this directly.
@@ -127,11 +129,12 @@ const getReactServerComponent = ({
127129
componentName,
128130
componentProps,
129131
railsContext,
132+
enforceRefetch = false,
130133
}: ClientGetReactServerComponentProps) => {
131134
assertRailsContextWithComponentSpecificMetadata(railsContext);
132135
const componentKey = createRSCPayloadKey(componentName, componentProps, railsContext);
133136
const payloads = window.REACT_ON_RAILS_RSC_PAYLOADS?.[componentKey];
134-
if (payloads) {
137+
if (!enforceRefetch && payloads) {
135138
return createFromPreloadedPayloads(payloads);
136139
}
137140
return fetchRSC({ componentName, componentProps, railsContext });

node_package/src/getReactServerComponent.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import transformRSCStream from './transformRSCNodeStream.ts';
44
import loadJsonFile from './loadJsonFile.ts';
55
import { assertRailsContextWithServerComponentCapabilities, RailsContext } from './types/index.ts';
66

7-
type RSCServerRootProps = {
7+
type GetReactServerComponentOnServerProps = {
88
componentName: string;
99
componentProps: unknown;
1010
railsContext: RailsContext;
@@ -46,6 +46,7 @@ const createFromReactOnRailsNodeStream = async (
4646
* @param componentName - Name of the server component to render
4747
* @param componentProps - Props to pass to the server component
4848
* @param railsContext - Context for the current request
49+
* @param enforceRefetch - Whether to enforce a refetch of the component
4950
* @returns A Promise resolving to the rendered React element
5051
*
5152
* @important This is an internal function. End users should not use this directly.
@@ -57,7 +58,7 @@ const getReactServerComponent = async ({
5758
componentName,
5859
componentProps,
5960
railsContext,
60-
}: RSCServerRootProps) => {
61+
}: GetReactServerComponentOnServerProps) => {
6162
assertRailsContextWithServerComponentCapabilities(railsContext);
6263

6364
if (typeof ReactOnRails.getRSCPayloadStream !== 'function') {

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"react-server": "./node_package/lib/wrapServerComponentRenderer/server.rsc.js",
2222
"default": "./node_package/lib/wrapServerComponentRenderer/server.js"
2323
},
24-
"./RSCRoute": "./node_package/lib/RSCRoute.js"
24+
"./RSCRoute": "./node_package/lib/RSCRoute.js",
25+
"./RSCProvider": "./node_package/lib/RSCProvider.js",
26+
"./ServerComponentFetchError": "./node_package/lib/ServerComponentFetchError.js"
2527
},
2628
"directories": {
2729
"doc": "docs"

0 commit comments

Comments
 (0)