React Server Components — что это?
Буквально неделю назад, команда реакт опубликовала в своем блоге о новом RFC. Давайте разберемся, что это за зверь и зачем он нужен.
Что это
Как понятно из названия React Server Components - это компоненты которые исполняются на сервере. Теперь у нас есть несколько видов компонентов:
Клиентские компоненты
Серверные компоненты
Гибридные компоненты
Клиентские компоненты
Это те компоненты которые мы пишем сейчас, используя состояние и эффекты, они позволяют взаимодействовать с пользователем и исполняются в браузере. До этого момента у нас существовали только клиентские компоненты. Теперь клиентские компоненты имеют постфикс в названии .client.js
Серверные компоненты
Это те компоненты которые исполняются на сервере, они не могут использовать состояние и эффекты(в них запрещено использование useState
и любых других "клиентских" хуков). Они могут быть перезапрошены по ходу исполнения приложения. Имеют доступ ко всей инфраструктуре сервера. Такие компоненты имеют постфикс в названии .server.js
. Также в качестве параметров не может передавать функции, только данные.
Гибридные компоненты
Эти компоненты могут исполняться как на сервере так и на клиенте. У них самые сильные ограничения. Они не могут использовать клиентские хуки и серверную инфраструктуру. Фактически могут только содержать JSX разметку.
Различие с SSR
Дочитав до этого момента многие из вас спросят себя, а в чем различие с SSR
и инструментами вроде Next.js
. Ведь реакт и раньше умел исполняться на сервере, верно? Не совсем так. Различие заключается в том, что SSR
возвращает нам HTML
разметку, которую после этого необходимо гидрировать. Чаще всего SSR
используется для первой загрузки приложения, чтобы пользователь не видел белый экран. При этом Server Components возвращает JSON
структуру части virtual dom
.
Картинка слева, это пример того, что передается по сети при использовании server components.
На данный момент ничего не сказано о совмещении SSR и server components, но я думаю в будущем эти два подхода можно будет совмещать.
Пример использования
Картинка выше демонстрирует, то как выглядит дерево компонентов. Раньше оно всегда состояло из клиентских компонент, теперь дерево может содержать серверные компоненты.
При готовом окружении, чтобы начать использовать серверные компоненты, необходимо создать файл с постфиксом .server.js
// Note.server.js - Server Component
import db from 'db.server';
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
// (B) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing
? <NoteEditor note={note} />
: null
}
</div>
);
}
Вот мы и создали первый серверный компонент.
Зачем нужно
Когда добавляют новые фичи это прикольно, но у них должна быть практическая польза, вот что описывает команда реакт:
Zero-Bundle-Size Components
Все мы любим зависимости в нашем приложении, но иногда их становится так много, что счет объема у приложения идет на десятки мегабайт. Так как мы получаем доступ к выполнению на сервере, то теперь и наши зависимости могут выполнять свои функции на сервере, без передачи исходного кода на клиент. Представьте, что вы пишите приложения для отображения markdown текста, сейчас вы напишите так:
// NoteWithMarkdown.js
// NOTE: *before* Server Components
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
При это к размеру bundle добавиться 74кб дополнительно, но если этот компонент мы превратим с server components, то получим следующее:
// NoteWithMarkdown.server.js - Server Component === zero bundle size
import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
В данном примере изменилось только название файла, но при этом мы экономим 74кб исполняя данную часть кода на сервере.
Полный доступ к Backend
Так как компоненты исполняются на сервере, можно получать полный доступ к окружению сервера:
// Note.server.js - Server Component
import fs from 'react-fs';
function Note({id}) {
const note = JSON.parse(fs.readFile(`${id}.json`));
return <NoteWithMarkdown note={note} />;
}
// Note.server.js - Server Component
import db from 'db.server';
function Note({id}) {
const note = db.notes.get(id);
return <NoteWithMarkdown note={note} />;
}
Только для доступа к окружения сервера, требуются обертки для реакта, на данный момент команда реакта разработала 3 обертки:
react-fs
- Обертка для работы с файламиreact-fetch
- Обертка для работы с сетьюreact-pg
- Обертка для работы с PostgresSql
Думаю в ближайшем будущем, у нас появится API с помощью которого можно будет создавать собственный обертки для работы с server component.
Автоматический Code Splitting
Если вы работаете с реакт, то вы уже знакомы с понятием Code Splitting. Это процесс когда компоненты загружаются по необходимости, для того, чтобы уменьшить размер нашего bundle мы используем динамические модули и React.lazy:
// PhotoRenderer.js
// NOTE: *before* Server Components
import React from 'react';
// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
При использовании Server Components, мы получаем данную возможность по умолчанию:
// PhotoRenderer.server.js - Server Component
import React from 'react';
// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
Так как данный компонент исполняется на сервере, то на клиент будет отправлен уже необходимый компонент, что позволяет писать более привычный и интуитивный код.
Отсутствие client-server водопадов
Водопад - это когда нам после рендера необходимо запросить данные с сервера, после их получения, возможно нам требуется получить еще какие то данные и таким образом нам необходимо выполняться множество запросов на сервер, а пользователю ожидать выполнения этих запросов.
Такой подход ухудшает пользовательский опыт при использовании приложения. Существуют разные решения, чаще всего они касаются API. Например graphql/JSON API и другие.
Чаще всего мы пишем компоненты следующим образом:
// Note.js
// NOTE: *before* Server Components
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
// NOTE: loads *after* rendering, triggering waterfalls in children
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return (/* render note here... */);
}
}
Тут мы загрузили сам компонент Note, после этого выполнили рендер и только после этого запросили данные с сервера. Теперь с server components мы можем это делать не последовательно, а одновременно:
// Note.server.js - Server Component
function Note(props) {
// NOTE: loads *during* render, w low-latency data access on the server
const note = db.notes.get(props.id);
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
Таким образом, когда компонент исполняется на сервере, он может выполнить взаимодействие с необходимым апи и передать данные в компонент за одну итерацию по сети для пользователя, после чего результат выполнения компонента стримится пользователю
Итог
На мой взгляд server components имеет место быть, сочетая Suspence, Concurent Mode и Server Components можно гибко для разработчиков и удобно для пользователя реализовывать UI.
Не забывайте это RFC и подход, реализации и API может поменяться до официального релиза.
Что думаете по поводу Server Components?
Дополнительный материал
Если хотите более детально погрузиться в тему Server Components.