Библиотека визуализации данных Nivo построена поверх React и активно использует размеры DOM-элементов, вычисления layout и браузерные API. Это делает её чувствительной к различиям между серверным и клиентским рендерингом.
При использовании серверного рендеринга (SSR), например в Next.js, возникает фундаментальное ограничение: сервер не имеет доступа к DOM. Это означает отсутствие:
windowdocumentoffsetWidth,
clientHeight)ResizeObserverNivo же, напротив, рассчитывает графики исходя из реальных размеров контейнера. Поэтому при SSR возникает необходимость корректной гидратации — синхронизации серверного HTML с клиентским React-деревом и последующего пересчёта визуализации.
Гидратация в React — процесс, при котором клиент «оживляет» HTML, сгенерированный сервером. Для Nivo ключевая проблема заключается в том, что серверный рендер не может знать реальные размеры контейнера графика.
Это приводит к следующим эффектам:
Особенно это заметно в компонентах:
ResponsiveBarResponsiveLineResponsivePieResponsiveScatterPlotЭти компоненты зависят от контейнера и не могут корректно вычисляться без клиентского измерения.
Responsive-компоненты в Nivo устроены следующим образом:
ResizeObserver)Ключевой момент: первый расчёт невозможен до монтирования компонента в браузере.
Поэтому SSR-рендер фактически не может содержать финальный SVG-график.
Наиболее распространённый подход — исключение Nivo-компонентов из серверного рендера.
В контексте Next.js используется динамический импорт:
import dynamic from "next/dynamic";
const ResponsiveBar = dynamic(
() => import("@nivo/bar").then(mod => mod.ResponsiveBar),
{ ssr: false }
);
Механизм ssr: false гарантирует, что компонент будет
создан только на клиенте, минуя стадию серверного HTML.
Альтернативный подход — использование состояния монтирования:
import { useState, useEffect } from "react";
import { ResponsiveLine } from "@nivo/line";
export default function ChartWrapper({ data }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <ResponsiveLine data={data} />;
}
Здесь график не участвует в SSR-дереве вообще. Это полностью исключает гидратационные конфликты, но увеличивает пустое пространство до момента монтирования.
В некоторых случаях требуется сохранить SSR-рендер и избежать скачков layout. Тогда используется фиксированный контейнер:
<div style={{ height: 400, width: "100%" }}>
<ResponsiveBar data={data} />
</div>
Этот подход позволяет:
Однако сам график всё равно перерисуется на клиенте после измерения.
Nivo предоставляет non-responsive версии компонентов:
BarLinePieОни не вычисляют размеры контейнера автоматически и требуют явного
задания width и height.
Пример:
import { Bar } from "@nivo/bar";
<Bar
width={600}
height={400}
data={data}
/>
Такая модель:
Минус — потеря адаптивности.
Даже при корректной гидратации может возникнуть расхождение:
Причина чаще всего связана с:
В таких случаях Nivo может полностью пересоздать SVG вместо патч-обновления.
Ключевой механизм Nivo — динамическое измерение контейнера.
Упрощённая модель работы:
ResizeObserverВажно учитывать:
Часто графики Nivo получают данные асинхронно. Это усиливает проблему гидратации:
Типичный сценарий:
const [data, setData] = useState([]);
useEffect(() => {
fetch("/api/data")
.then(r => r.json())
.then(setData);
}, []);
Результат:
Для уменьшения гидратационных конфликтов используется стабилизация входных данных:
useMemo для предотвращения пересоздания массиваconst stableData = useMemo(() => data, [data]);
Ключевая цель — обеспечить идентичность входных props между SSR и CSR фазами.
В сложных интерфейсах графики часто рендерятся условно:
В SSR это создаёт дополнительные риски:
Рекомендуемая модель — унификация условий рендера:
React StrictMode усиливает эффект гидратации:
В результате:
Это не ошибка Nivo, а следствие React-режима разработки.
Наиболее стабильная архитектура:
Структурно это выглядит как:
Такой подход полностью устраняет гидратационные конфликты, сохраняя производительность и предсказуемость интерфейса.