Renderização no servidor
A renderização no servidor (SSR) pode melhorar o desempenho do primeiro carregamento da sua aplicação. O Reactive Data Client vai um passo além ao preencher previamente o store de dados. Diferentemente de outras metodologias de SSR, o Reactive Data Client se torna interativo no instante em que a página fica visível, tornando as mutações de dados instantâneas. Além disso, não há necessidade de fetches de dados adicionais, que aumentam a carga do servidor e atrasam a hidratação no cliente, podendo causar travamentos na aplicação.
NextJS SSR
App Router
O NextJS 12 inclui uma nova forma de roteamento no diretório '/app'. Isso permite mais melhorias de desempenho, além de roteamento dinâmico e aninhado.
Root Layout
Coloque o DataProvider no seu root layout
import { DataProvider } from '@data-client/react/nextjs';
import { AsyncBoundary } from '@data-client/react';
export default function RootLayout({ children }) {
return (
<html>
<body>
<DataProvider>
<header>Title</header>
<AsyncBoundary>{children}</AsyncBoundary>
<footer></footer>
</DataProvider>
</body>
</html>
);
}
Client Components
Para manter seus dados atualizados e com bom desempenho, você pode usar client components e useSuspense()
'use client';
import { useSuspense } from '@data-client/react';
import { TodoResource } from '@/resources/Todo';
import TodoList from '@/components/TodoList';
export default function InteractivePage({ params }: { params: { userId: number } }) {
const todos = useSuspense(TodoResource.getList, params);
return <TodoList todos={todos} />;
}
Observe que isso é idêntico a como você escreveria componentes sem SSR. Isso torna os componentes utilizáveis em várias plataformas.
Server Components
No entanto, se seus dados nunca mudam, você pode reduzir um pouco o bundle de javascript enviado
usando um server component. Basta usar await no endpoint:
import { TodoResource } from '@/resources/Todo';
import TodoList from '@/components/TodoList';
export default async function StaticPage({ params }: { params: { userId: number } }) {
const todos = await TodoResource.getList(params);
return <TodoList todos={todos} />;
}
Demo
Explore o exemplo nextjs
Class mangling e Entity.key
O NextJS renomeia classes nos builds de produção. Por causa disso, é fundamental definir Entity.key, já que sua implementação padrão se baseia no nome da classe.
class User extends Entity {
id = '';
username = '';
static key = 'User';
}
Pages Router
Com o NextJS < 14, você pode estar usando o pages router. Para isso, temos um Document e um wrapper específico do NextJS para o App
- NPM
- Yarn
- pnpm
- esm.sh
yarn add @data-client/ssr @data-client/redux redux
npm install --save @data-client/ssr @data-client/redux redux
pnpm add @data-client/ssr @data-client/redux redux
<script type="module">
import * from 'https://esm.sh/@data-client/ssr';
import * from 'https://esm.sh/@data-client/redux';
import * from 'https://esm.sh/redux';
</script>
import { DataClientDocument } from '@data-client/ssr/nextjs';
export default DataClientDocument;
import { AppDataProvider } from '@data-client/ssr/nextjs';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<AppDataProvider>
<Component {...pageProps} />
</AppDataProvider>
);
}
Ao buscar dados a partir de parâmetros de useRouter(), você precisará adicionar getServerSideProps para evitar que o NextJS defina router.query como vazio
export default function MyComponent() {
const id: string; = useRouter().query.id;
const post = useSuspense(getPost, { id });
// etc
}
export const getServerSideProps = () => ({ props: {} });
Personalizando ainda mais o Document
Para personalizar ainda mais o Document, basta estender o document fornecido.
Certifique-se de usar super.getInitialProps() em vez de Document.getInitialProps(),
ou o código do Reactive Data Client não será executado!
import { Html, Head, Main, NextScript } from 'next/document';
import { DataClientDocument } from '@data-client/ssr/nextjs';
export default class MyDocument extends DataClientDocument {
static async getInitialProps(ctx) {
const originalRenderPage = ctx.renderPage;
// Run the React rendering logic synchronously
ctx.renderPage = () =>
originalRenderPage({
// Useful for wrapping the whole react tree
enhanceApp: App => App,
// Useful for wrapping in a per-page basis
enhanceComponent: Component => Component,
});
// Run the parent `getInitialProps`, it now includes the custom `renderPage`
const initialProps = await super.getInitialProps(ctx);
return initialProps;
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
CSP Nonce
O Document do Reactive Data Client serializa o estado do store em uma tag script. Caso você tenha
restrições de Content Security Policy que exijam o uso de um nonce, você pode sobrescrever
DataClientDocument.getNonce.
Como não há uma forma padrão de lidar com nonce no NextJS, isso permite recuperar qualquer nonce que você tenha criado no DocumentContext para usar com o Reactive Data Client.
import { DataClientDocument } from '@data-client/ssr/nextjs';
import type { DocumentContext } from 'next/document.js';
export default class MyDocument extends DataClientDocument {
static getNonce(ctx: DocumentContext & { res: { nonce?: string } }) {
// this assumes nonce has been added here - customize as you need
return ctx?.res?.nonce;
}
}
SSR com Express JS
Ao implementar seu próprio servidor usando express.
Lado do servidor
import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import {
createPersistedStore,
createServerDataComponent,
} from '@data-client/react/ssr';
import App from './App';
import Document from './Document';
import assets from './assets';
import { NeededForPage } from './resources/NeededForPage';
const rootId = 'react-root';
const PORT = 3000;
const app = express();
app.get('/*', (req: any, res: any) => {
let didError = false;
const [ServerDataProvider, useReadyCacheState, controller] =
createPersistedStore();
const ServerDataComponent =
createServerDataComponent(useReadyCacheState);
controller.fetch(NeededForPage, { id: 5 });
const { pipe, abort } = renderToPipeableStream(
<Document
assets={assets}
scripts={[<ServerDataComponent key="server-data" />]}
rootId={rootId}
>
<ServerDataProvider>
<App />
</ServerDataProvider>
</Document>,
{
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onError(x: any) {
didError = true;
console.error(x);
res.statusCode = 500;
pipe(res);
},
},
);
// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(abort, 1000);
});
app.listen(PORT, () => {
console.log(`Listening at ${PORT}...`);
});
Cliente
import { hydrateRoot } from 'react-dom/client';
import { DataProvider } from '@data-client/react';
import { awaitInitialData } from '@data-client/react/ssr';
import App from './App';
const rootId = 'react-root';
awaitInitialData().then(initialState => {
hydrateRoot(
document.getElementById(rootId)!,
<DataProvider initialState={initialState}>
<App />
</DataProvider>,
);
});