Библиотека Nivo предназначена для визуализации данных в React-приложениях. Большинство графиков работают не со статическими массивами, а с удалёнными API, серверной аналитикой, потоковыми обновлениями и кэшируемыми запросами. Для организации загрузки данных удобно использовать TanStack Query — современную библиотеку управления серверным состоянием.
Связка React Query и Nivo решает несколько задач одновременно:
npm install @tanstack/react-query
npm install @nivo/bar
npm install @nivo/line
npm install @nivo/pie
Для работы React Query необходимо подключить
QueryClientProvider.
import React from "react";
import ReactDOM from "react-dom/client";
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import App from "./App";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
QueryClient отвечает за:
async function fetchSales() {
const response = await fetch("/api/sales");
if (!response.ok) {
throw new Error("Ошибка загрузки");
}
return response.json();
}
import { useQuery } from "@tanstack/react-query";
function SalesChart() {
const {
data,
isLoading,
error,
} = useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
});
if (isLoading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка загрузки данных</div>;
}
return (
<pre>
{JSON.stringify(data, null, 2)}
</pre>
);
}
Предположим, API возвращает:
[
{
"month": "Jan",
"profit": 120,
"loss": 40
},
{
"month": "Feb",
"profit": 180,
"loss": 70
}
]
import { ResponsiveBar } from "@nivo/bar";
function SalesChart() {
const {
data,
isLoading,
error,
} = useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
});
if (isLoading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка</div>;
}
return (
<div style={{ height: 500 }}>
<ResponsiveBar
data={data}
keys={["profit", "loss"]}
indexBy="month"
margin={{
top: 50,
right: 130,
bottom: 50,
left: 60,
}}
padding={0.3}
colors={{ scheme: "nivo" }}
axisBottom={{
legend: "Месяц",
legendPosition: "middle",
legendOffset: 32,
}}
axisLeft={{
legend: "Сумма",
legendPosition: "middle",
legendOffset: -40,
}}
/>
</div>
);
}
При работе с графиками важно учитывать:
function SalesChart() {
const {
data,
isLoading,
isFetching,
error,
} = useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
});
if (isLoading) {
return <Spinner />;
}
if (error) {
return <ErrorMessage />;
}
if (!data?.length) {
return <EmptyState />;
}
return (
<>
{isFetching && (
<div>Обновление...</div>
)}
<div style={{ height: 400 }}>
<ResponsiveBar
data={data}
keys={["profit"]}
indexBy="month"
/>
</div>
</>
);
}
| Состояние | Описание |
|---|---|
isLoading |
Первый запрос |
isFetching |
Любой активный запрос |
isRefetching |
Повторная загрузка |
React Query поддерживает периодическое обновление данных.
const query = useQuery({
queryKey: ["stats"],
queryFn: fetchStats,
refetchInterval: 5000,
});
Теперь график обновляется каждые 5 секунд.
import { ResponsiveLine } from "@nivo/line";
function RealtimeChart() {
const { data } = useQuery({
queryKey: ["metrics"],
queryFn: fetchMetrics,
refetchInterval: 2000,
});
return (
<div style={{ height: 500 }}>
<ResponsiveLine
data={data}
margin={{
top: 50,
right: 110,
bottom: 50,
left: 60,
}}
xScale={{
type: "point",
}}
yScale={{
type: "linear",
min: "auto",
max: "auto",
}}
curve="monotoneX"
enablePoints={false}
useMesh={true}
/>
</div>
);
}
API редко возвращает данные в формате, подходящем для Nivo.
[
{
"date": "2025-01-01",
"value": 120
},
{
"date": "2025-01-02",
"value": 180
}
]
const chartData = [
{
id: "Продажи",
data: [
{
x: "2025-01-01",
y: 120,
},
{
x: "2025-01-02",
y: 180,
},
],
},
];
async function fetchChartData() {
const response = await fetch("/api/chart");
const result = await response.json();
return [
{
id: "Продажи",
data: result.map(item => ({
x: item.date,
y: item.value,
})),
},
];
}
React Query позволяет трансформировать данные через
select.
const query = useQuery({
queryKey: ["chart"],
queryFn: fetchChart,
select: (data) => {
return [
{
id: "Revenue",
data: data.map(item => ({
x: item.date,
y: item.total,
})),
},
];
},
});
Преимущества:
По умолчанию React Query кэширует данные.
useQuery({
queryKey: ["analytics"],
queryFn: fetchAnalytics,
staleTime: 60000,
});
Определяет время актуальности кэша.
staleTime: 60000
Данные считаются свежими 60 секунд.
useQuery({
queryKey: ["stats"],
queryFn: fetchStats,
gcTime: 300000,
});
Время хранения неиспользуемого кэша.
Иногда страница содержит несколько диаграмм.
const usersQuery = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
});
const salesQuery = useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
});
const ordersQuery = useQuery({
queryKey: ["orders"],
queryFn: fetchOrders,
});
Для большого количества запросов удобнее использовать
useQueries.
import { useQueries } from "@tanstack/react-query";
const results = useQueries({
queries: [
{
queryKey: ["sales"],
queryFn: fetchSales,
},
{
queryKey: ["users"],
queryFn: fetchUsers,
},
],
});
Большие временные диапазоны часто загружаются частями.
const query = useQuery({
queryKey: ["history", page],
queryFn: () => fetchHistory(page),
});
function RevenueChart({ year }) {
const query = useQuery({
queryKey: ["revenue", year],
queryFn: () => fetchRevenue(year),
});
}
При изменении year React Query автоматически выполнит
новый запрос.
function Dashboard({ range }) {
const query = useQuery({
queryKey: ["dashboard", range],
queryFn: () => fetchDashboard(range),
});
}
import { useQueryClient } from "@tanstack/react-query";
function DashboardLink() {
const queryClient = useQueryClient();
const preload = () => {
queryClient.prefetchQuery({
queryKey: ["analytics"],
queryFn: fetchAnalytics,
});
};
return (
<button onMouseEn ter={preload}>
Dashboard
</button>
);
}
После изменения данных необходимо обновить кэш.
import { useMutation } from "@tanstack/react-query";
const mutation = useMutation({
mutationFn: createSale,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["sales"],
});
},
});
React Query не зависит от конкретного HTTP-клиента.
npm install axios
import axios from "axios";
async function fetchStats() {
const response = await axios.get("/api/stats");
return response.data;
}
React Query поддерживает AbortController.
async function fetchData({ signal }) {
const response = await fetch(
"/api/chart",
{ signal }
);
return response.json();
}
const query = useQuery({
queryKey: ["chart"],
queryFn: fetchChart,
retry: 3,
retryDelay: 1000,
});
| Параметр | Описание |
|---|---|
retry |
Количество попыток |
retryDelay |
Задержка между попытками |
function useSalesChart() {
return useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
select: (data) => {
return data.map(item => ({
month: item.month,
revenue: item.revenue,
}));
},
});
}
function SalesChart() {
const { data } = useSalesChart();
return (
<ResponsiveBar
data={data}
keys={["revenue"]}
indexBy="month"
/>
);
}
Сложные API могут содержать вложенные структуры.
{
"meta": {},
"payload": {
"items": [
{
"day": "Mon",
"count": 12
}
]
}
}
select: (response) => {
return response.payload.items.map(
item => ({
day: item.day,
count: item.count,
})
);
}
Для графиков лучше использовать skeleton UI.
function ChartSkeleton() {
return (
<div
style={{
height: 400,
background: "#f5f5f5",
}}
/>
);
}
Тяжёлые графики можно загружать динамически.
import { lazy, Suspense } from "react";
const AnalyticsChart = lazy(() =>
import("./AnalyticsChart")
);
function Page() {
return (
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
);
}
import { memo } from "react";
const Chart = memo(function Chart({
data,
}) {
return (
<ResponsiveBar
data={data}
keys={["value"]}
indexBy="label"
/>
);
});
Nivo активно реагирует на изменение ссылок объектов.
Плохой вариант:
<ResponsiveBar
data={[...data]}
/>
Хороший вариант:
<ResponsiveBar
data={data}
/>
const normalizedData = useMemo(() => {
return data.map(item => ({
x: item.date,
y: item.value,
}));
}, [data]);
При использовании:
React Query позволяет гидрировать кэш.
import {
dehydrate,
QueryClient,
} from "@tanstack/react-query";
const query = useSuspenseQuery({
queryKey: ["chart"],
queryFn: fetchChart,
});
const query = useQuery({
queryKey: ["heavy-chart"],
queryFn: fetchChart,
enabled: isVisible,
});
Запрос графика может зависеть от другого запроса.
const userQuery = useQuery({
queryKey: ["user"],
queryFn: fetchUser,
});
const analyticsQuery = useQuery({
queryKey: ["analytics", userQuery.data?.id],
queryFn: () =>
fetchAnalytics(userQuery.data.id),
enabled: !!userQuery.data,
});
import {
useInfiniteQuery,
} from "@tanstack/react-query";
const query = useInfiniteQuery({
queryKey: ["events"],
queryFn: fetchEvents,
getNextPageParam: (lastPage) => {
return lastPage.nextCursor;
},
});
const chartData = query.data.pages.flatMap(
page => page.items
);
npm install @tanstack/react-query-devtools
import {
ReactQueryDevtools,
} from "@tanstack/react-query-devtools";
<ReactQueryDevtools initialIsOpen={false} />
Devtools позволяют:
Плохой пример:
data.push(newItem);
Хороший пример:
const newData = [...data, newItem];
Плохой вариант:
queryKey: ["chart", {}]
Хороший вариант:
queryKey: ["chart", year]
Плохой вариант:
const result = hugeTransform(data);
Лучше:
const result = useMemo(() => {
return hugeTransform(data);
}, [data]);
Распространённая структура:
src/
├── api/
├── hooks/
├── charts/
├── dashboard/
├── queries/
└── utils/
| Каталог | Назначение |
|---|---|
api |
HTTP-запросы |
hooks |
React Query hooks |
charts |
Nivo-компоненты |
queries |
queryKey и конфигурации |
utils |
трансформации данных |
export async function fetchSales() {
const response = await fetch(
"/api/sales"
);
return response.json();
}
export function useSales() {
return useQuery({
queryKey: ["sales"],
queryFn: fetchSales,
});
}
export function SalesChart() {
const { data } = useSales();
return (
<ResponsiveBar
data={data}
keys={["revenue"]}
indexBy="month"
/>
);
}
При работе с тысячами точек:
<ResponsiveLine
animate={false}
/>
const [range, setRange] = useState("");
const debouncedRange = useDebounce(
range,
500
);
const query = useQuery({
queryKey: ["chart", debouncedRange],
queryFn: () =>
fetchChart(debouncedRange),
});
Nivo поддерживает временные шкалы.
xScale={{
type: "time",
format: "%Y-%m-%d",
precision: "day",
}}
axisBottom={{
format: "%b %d",
tickValues: "every 2 days",
}}
Иногда сервер возвращает слишком детализированные данные.
[
{
"timestamp": "10:01",
"value": 10
},
{
"timestamp": "10:02",
"value": 11
}
]
function aggregate(data) {
const map = {};
data.forEach(item => {
const hour =
item.timestamp.slice(0, 2);
if (!map[hour]) {
map[hour] = 0;
}
map[hour] += item.value;
});
return Object.entries(map).map(
([hour, value]) => ({
hour,
value,
})
);
}