Skip to content

Commit

Permalink
feat: Add @data-client/react/nextjs entrypoint (#3093)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 13, 2024
1 parent c69cf0b commit a998a0b
Show file tree
Hide file tree
Showing 23 changed files with 1,488 additions and 7 deletions.
21 changes: 21 additions & 0 deletions .changeset/brave-bottles-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@data-client/react': patch
---

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

```tsx
import { DataProvider } from '@data-client/react/nextjs';

export default function RootLayout({ children }) {
return (
<html>
<body>
<DataProvider>
{children}
</DataProvider>
</body>
</html>
);
}
```
2 changes: 1 addition & 1 deletion docs/core/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ performance improvements, as well as dynamic and nested routing.
Place [DataProvider](https://dataclient.io/docs/api/DataProvider) in your [root layout](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required)

```tsx title="app/layout.tsx"
import { DataProvider } from '@data-client/ssr/nextjs';
import { DataProvider } from '@data-client/react/nextjs';
import { AsyncBoundary } from '@data-client/react';

export default function RootLayout({ children }) {
Expand Down
5 changes: 1 addition & 4 deletions docs/core/shared/_installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,12 @@ Alternatively [integrate state with redux](../guides/redux.md)

<TabItem value="nextjs">

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

<p style={{textAlign: 'center'}}>
<Link className="button button--primary" to="../guides/ssr#nextjs">Full NextJS Guide</Link>
</p>

```tsx title="app/layout.tsx"
import { DataProvider } from '@data-client/ssr/nextjs';
import { AsyncBoundary } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';

export default function RootLayout({ children }) {
return (
Expand Down
1 change: 1 addition & 0 deletions packages/react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/legacy
/index.d.ts
/next.d.ts
/nextjs.d.ts
7 changes: 7 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"next": [
"lib/next/index.d.ts"
],
"nextjs": [
"lib/server/nextjs/index.d.ts"
],
"*": [
"lib/index.d.ts"
]
Expand Down Expand Up @@ -92,6 +95,10 @@
"require": "./dist/next.js",
"default": "./lib/next/index.js"
},
"./nextjs": {
"types": "./lib/server/nextjs/index.d.ts",
"default": "./lib/server/nextjs/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 @@ -57,6 +57,11 @@ if (process.env.BROWSERSLIST_ENV !== 'node12') {
});
configs.push(typeConfig);
configs.push(typeConfigNext);
configs.push({
...typeConfig,
input: './lib/server/nextjs/index.d.ts',
output: [{ file: 'nextjs.d.ts', format: 'es' }],
});
} else {
// node-friendly commonjs build
[
Expand Down
31 changes: 31 additions & 0 deletions packages/react/src/server/ServerData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { State } from '@data-client/core';

export const ServerData = ({
data,
nonce,
id = 'data-client-data',
}: {
data: State<unknown>;
id?: string;
nonce?: string | undefined;
}) => {
try {
const encoded = JSON.stringify(data);
return (
<script
id={id}
type="application/json"
dangerouslySetInnerHTML={{
__html: encoded,
}}
nonce={nonce}
/>
);
} catch (e) {
console.error(`Error serializing json for ${id}`);
console.error(e);
return null;
}
};

export default ServerData;
52 changes: 52 additions & 0 deletions packages/react/src/server/getInitialData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { initialState } from '@data-client/core';

export const awaitInitialData = (id = 'data-client-data'): Promise<any> => {
return new Promise<any>((resolve, reject) => {
let el: HTMLScriptElement | null;
if ((el = document.getElementById(id) as any)) {
resolve(getDataFromEl(el, id));
return;
}
document.addEventListener('DOMContentLoaded', () => {
el = document.getElementById(id) as any;
if (el) resolve(getDataFromEl(el, id));
else
reject(new Error('failed to find DOM with reactive data client state'));
});
});
};

export const getInitialData = (id = 'data-client-data') => {
const el: HTMLScriptElement | null = document.getElementById(id) as any;
if (!el) return initialState;
return getDataFromEl(el, id);
};

function getDataFromEl(el: HTMLScriptElement, key: string) {
if (el.text === undefined) {
console.error(
`#${key} is completely empty. This could be due to CSP issues.`,
);
}
if (getInitialData.name !== 'getInitialData') {
(document as any).FUNC_MANGLE = function () {
console.error(
'Data Client Error: https://dataclient.io/errors/osid',
this,
);
delete (document as any).FUNC_MANGLE;
};
}
if (Test.name !== 'Test') {
(document as any).CLS_MANGLE = function () {
console.error(
'Data Client Error: https://dataclient.io/errors/dklj',
this,
);
delete (document as any).CLS_MANGLE;
};
}
return el?.text ? JSON.parse(el?.text) : undefined;
}

class Test {}
29 changes: 29 additions & 0 deletions packages/react/src/server/nextjs/DataProvider/DataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';
import { useMemo, type ComponentProps } from 'react';

import createPersistedStore from './createPersistedStore.js';
import ServerDataComponent from './ServerDataComponent.js';
import type CacheProvider from '../../../components/CacheProvider.js';

export default function DataProvider({
children,
...props
}: ProviderProps): React.ReactElement {
const [ServerCacheProvider, initPromise] = useMemo(createPersistedStore, []);

return (
<>
<ServerDataComponent initPromise={initPromise} />
<ServerCacheProvider {...props} initPromise={initPromise}>
{children}
</ServerCacheProvider>
</>
);
}

type ProviderProps = Omit<
Partial<ComponentProps<typeof CacheProvider>>,
'initialState'
> & {
children: React.ReactNode;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { use } from 'react';

import ServerData from '../../ServerData.js';

const id = 'data-client-data';

const ServerDataComponent = ({
nonce,
initPromise,
}: {
nonce?: string | undefined;
initPromise: Promise<any>;
}) => {
const data = use(initPromise);
return <ServerData data={data} id={id} nonce={nonce} />;
};
export default ServerDataComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import createPersistedStoreClient from './createPersistedStoreClient.js';
import createPersistedStoreServer from './createPersistedStoreServer.js';

const createPersistedStore =
typeof window === 'undefined' ?
createPersistedStoreServer
: createPersistedStoreClient;
export default createPersistedStore;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/// <reference types="react/canary" />
import { type State } from '@data-client/core';
import { ComponentProps, use } from 'react';

import CacheProvider from '../../../components/CacheProvider.js';
import { awaitInitialData } from '../../getInitialData.js';

export default function createPersistedStore() {
const initPromise = awaitInitialData();

const StoreCacheProvider = ({
children,
initPromise,
...props
}: ProviderProps) => {
const initialState = use(initPromise);
return (
<CacheProvider {...props} initialState={initialState}>
{children}
</CacheProvider>
);
};
return [StoreCacheProvider, initPromise] as const;
}

type ProviderProps = Omit<
Partial<ComponentProps<typeof CacheProvider>>,
'initialState'
> & {
children: React.ReactNode;
initPromise: Promise<State<any>>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Controller,
Manager,
NetworkManager,
State,
createReducer,
initialState,
applyManager,
} from '@data-client/core';
import type { ComponentProps } from 'react';

import type CacheProvider from '../../../components/CacheProvider.js';
import {
ExternalCacheProvider,
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());

const initPromise: Promise<State<any>> = (async () => {
let firstRender = true;
// eslint-disable-next-line no-constant-condition
while (true) {
const inFlightFetches = nm.allSettled();
if (inFlightFetches) {
firstRender = false;
await inFlightFetches;
continue;
}
if (firstRender) {
firstRender = false;
// TODO: instead of waiting 10ms - see if we can wait until next part of react is streamed and race with nm getting new fetches
await new Promise(resolve => setTimeout(resolve, 10));
continue;
}
break;
}
return getState();
})();

const StoreCacheProvider = ({ children }: ProviderProps) => {
return (
<ExternalCacheProvider
store={store}
selector={selector}
controller={controller}
>
{children}
</ExternalCacheProvider>
);
};

return [StoreCacheProvider, initPromise] as const;
}

type ProviderProps = Omit<
Partial<ComponentProps<typeof CacheProvider>>,
'initialState'
> & {
children: React.ReactNode;
initPromise: Promise<State<any>>;
};
1 change: 1 addition & 0 deletions packages/react/src/server/nextjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as DataProvider } from './DataProvider/DataProvider.js';

0 comments on commit a998a0b

Please sign in to comment.