Skip to content

Commit

Permalink
feat: Add /ssr entrypoint to react pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 14, 2024
1 parent 2b2fcf8 commit d1b9e96
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 9 deletions.
12 changes: 12 additions & 0 deletions .changeset/itchy-pumpkins-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@data-client/react': patch
---

Add /ssr entrypoint - eliminating the need for @data-client/ssr package completely

```ts
import {
createPersistedStore,
createServerDataComponent,
} from '@data-client/react/ssr';
```
17 changes: 9 additions & 8 deletions docs/core/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function RootLayout({ children }) {
<header>Title</header>
<AsyncBoundary>{children}</AsyncBoundary>
<footer></footer>
// highlight-next-line
// highlight-next-line
</DataProvider>
</body>
</html>
Expand Down Expand Up @@ -92,7 +92,9 @@ class User extends Entity {
id = '';
username = '';

pk() { return this.id }
pk() {
return this.id;
}

// highlight-next-line
static key = 'User';
Expand Down Expand Up @@ -212,8 +214,6 @@ export default class MyDocument extends DataClientDocument {

When implementing your own server using express.

<PkgTabs pkgs="@data-client/ssr @data-client/redux redux" />

### Server side

```tsx
Expand All @@ -222,15 +222,16 @@ import { renderToPipeableStream } from 'react-dom/server';
import {
createPersistedStore,
createServerDataComponent,
} from '@data-client/ssr';
} from '@data-client/react/ssr';

const rootId = 'react-root';

const app = express();
app.get('/*', (req: any, res: any) => {
const [ServerDataProvider, useReadyCacheState, controller] =
createPersistedStore();
const ServerDataComponent = createServerDataComponent(useReadyCacheState);
const ServerDataComponent =
createServerDataComponent(useReadyCacheState);

controller.fetch(NeededForPage, { id: 5 });

Expand Down Expand Up @@ -266,13 +267,13 @@ app.get('/*', (req: any, res: any) => {
app.listen(3000, () => {
console.log(`Listening at ${PORT}...`);
});
````
```

### Client

```tsx
import { hydrateRoot } from 'react-dom';
import { awaitInitialData } from '@data-client/ssr';
import { awaitInitialData } from '@data-client/react/ssr';

const rootId = 'react-root';

Expand Down
3 changes: 2 additions & 1 deletion packages/react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/legacy
/index.d.ts
/next.d.ts
/nextjs.d.ts
/nextjs.d.ts
/ssr.d.ts
10 changes: 10 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
"nextjs": [
"lib/server/nextjs/index.d.ts"
],
"ssr": [
"lib/server/index.d.ts"
],
"*": [
"lib/index.d.ts"
]
Expand All @@ -78,6 +81,9 @@
"next": [
"ts3.4/next/index.d.ts"
],
"ssr": [
"lib/server/index.d.ts"
],
"*": [
"ts3.4/index.d.ts"
]
Expand All @@ -99,6 +105,10 @@
"types": "./lib/server/nextjs/index.d.ts",
"default": "./lib/server/nextjs/index.js"
},
"./ssr": {
"types": "./lib/server/index.d.ts",
"default": "./lib/server/index.js"
},
"./package.json": "./package.json"
},
"type": "module",
Expand Down
5 changes: 5 additions & 0 deletions packages/react/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ if (process.env.BROWSERSLIST_ENV !== 'node12') {
input: './lib/server/nextjs/index.d.ts',
output: [{ file: 'nextjs.d.ts', format: 'es' }],
});
configs.push({
...typeConfig,
input: './lib/server/index.d.ts',
output: [{ file: 'ssr.d.ts', format: 'es' }],
});
} else {
// node-friendly commonjs build
[
Expand Down
65 changes: 65 additions & 0 deletions packages/react/src/server/createPersistedStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';
import {
Controller,
Manager,
NetworkManager,
State,
initialState,
createReducer,
applyManager,
} from '@data-client/core';
import { useSyncExternalStore } from 'react';

import { ExternalDataProvider, PromiseifyMiddleware } from './redux/index.js';
import { createStore, applyMiddleware } from './redux/redux.js';

export default function createPersistedStore(managers?: Manager[]) {
const controller = new Controller();
managers = managers ?? [new NetworkManager()];
const nm: NetworkManager = managers.find(
m => m instanceof NetworkManager,
) as any;
if (nm === undefined)
throw new Error('managers must include a NetworkManager');
const reducer = createReducer(controller);
const enhancer = applyMiddleware(
// redux 5's types are wrong and do not allow any return typing from next, which is incorrect.
// `next: (action: unknown) => unknown`: allows any action, but disallows all return types.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...applyManager(managers, controller),
PromiseifyMiddleware,
);
const store = createStore(reducer, initialState as any, enhancer);
managers.forEach(manager => manager.init?.(store.getState()));

const selector = (state: any) => state;

const getState = () => selector(store.getState());
let firstRender = true;
function useReadyCacheState(): State<unknown> {
const inFlightFetches = nm.allSettled();
if (inFlightFetches) {
firstRender = false;
throw inFlightFetches;
}
if (firstRender) {
firstRender = false;
throw new Promise(resolve => setTimeout(resolve, 10));
}
return useSyncExternalStore(store.subscribe, getState, getState);
}

function ServerDataProvider({ children }: { children: React.ReactNode }) {
return (
<ExternalDataProvider
store={store}
selector={selector}
controller={controller}
>
{children}
</ExternalDataProvider>
);
}
return [ServerDataProvider, useReadyCacheState, controller, store] as const;
}
20 changes: 20 additions & 0 deletions packages/react/src/server/createServerDataComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { State } from '@data-client/core';
import { Suspense } from 'react';

import ServerData from './ServerData.js';

export default function createServerDataComponent(
useReadyCacheState: () => State<unknown>,
id = 'data-client-data',
) {
const ServerDataAsync = ({ nonce }: { nonce?: string | undefined }) => {
const data = useReadyCacheState();
return <ServerData data={data} id={id} nonce={nonce} />;
};
const ServerDataComponent = ({ nonce }: { nonce?: string | undefined }) => (
<Suspense>
<ServerDataAsync nonce={nonce} />
</Suspense>
);
return ServerDataComponent;
}
9 changes: 9 additions & 0 deletions packages/react/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Object.hasOwn =
Object.hasOwn ||
/* istanbul ignore next */ function hasOwn(it, key) {
return Object.prototype.hasOwnProperty.call(it, key);
};
export { default as createPersistedStore } from './createPersistedStore.js';
export * from './getInitialData.js';
export { default as createServerDataComponent } from './createServerDataComponent.js';
export { default as ServerData } from './ServerData.js';
1 change: 1 addition & 0 deletions scripts/copywebsitetypes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ cp ./packages/rest/next.d.ts ./website/src/components/Playground/editor-types/@d
cp ./packages/core/next.d.ts ./website/src/components/Playground/editor-types/@data-client/core/next.d.ts
cp ./packages/react/next.d.ts ./website/src/components/Playground/editor-types/@data-client/react/next.d.ts
cp ./packages/react/nextjs.d.ts ./website/src/components/Playground/editor-types/@data-client/react/nextjs.d.ts
cp ./packages/react/ssr.d.ts ./website/src/components/Playground/editor-types/@data-client/react/ssr.d.ts
cp ./node_modules/@types/react/index.d.ts ./website/src/components/Playground/editor-types/react.d.ts
rm ./website/src/components/Playground/editor-types/globals.d.ts
yarn run rollup --config ./scripts/globals.rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as _data_client_core from '@data-client/core';
import { Manager, State, Controller } from '@data-client/core';
import * as react_jsx_runtime from 'react/jsx-runtime';

declare function createPersistedStore(managers?: Manager[]): readonly [({ children }: {
children: React.ReactNode;
}) => react_jsx_runtime.JSX.Element, () => State<unknown>, Controller<_data_client_core.DataClientDispatch>, undefined<State<unknown>, _data_client_core.ActionTypes, unknown> & {
dispatch: unknown;
}]

declare const awaitInitialData: (id?: string) => Promise<any>;
declare const getInitialData: (id?: string) => any;

declare function createServerDataComponent(useReadyCacheState: () => State<unknown>, id?: string): ({ nonce }: {
nonce?: string | undefined;
}) => react_jsx_runtime.JSX.Element;

declare const ServerData: ({ data, nonce, id, }: {
data: State<unknown>;
id?: string;
nonce?: string | undefined;
}) => react_jsx_runtime.JSX.Element | null;

export { ServerData, awaitInitialData, createPersistedStore, createServerDataComponent, getInitialData };

0 comments on commit d1b9e96

Please sign in to comment.