Библиотека Nivo активно использует декларативную модель React-компонентов. Диаграммы строятся на основе входных данных и автоматически обновляются при изменении состояния приложения. По этой причине выбор подхода к управлению состоянием напрямую влияет на производительность визуализаций, повторное использование логики, масштабируемость и предсказуемость обновлений графиков.
В типичном приложении с графиками состояние включает:
Пример структуры состояния:
{
charts: {
sales: [...],
users: [...],
revenue: [...]
},
filters: {
country: 'USA',
year: 2025
},
ui: {
darkMode: true,
animations: false
}
}
При использовании Nivo любые изменения таких данных вызывают перерисовку компонентов визуализации:
<ResponsiveLine data={data} />
Если data изменится — график обновится
автоматически.
Redux предоставляет централизованное хранилище состояния, где все данные приложения находятся в одном объекте store.
Redux особенно полезен в следующих сценариях:
Современный Redux практически всегда используется через Redux Toolkit.
npm install @reduxjs/toolkit react-redux
import { configureStore } from '@reduxjs/toolkit'
import chartsReducer from './chartsSlice'
export const store = configureStore({
reducer: {
charts: chartsReducer
}
})
Подключение:
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
salesData: [],
loading: false,
error: null
}
const chartsSlice = createSlice({
name: 'charts',
initialState,
reducers: {
setSalesData(state, action) {
state.salesData = action.payload
},
setLoading(state, action) {
state.loading = action.payload
},
setError(state, action) {
state.error = action.payload
}
}
})
export const {
setSalesData,
setLoading,
setError
} = chartsSlice.actions
export default chartsSlice.reducer
import { useSelector } from 'react-redux'
import { ResponsiveBar } from '@nivo/bar'
export default function SalesChart() {
const data = useSelector(
state => state.charts.salesData
)
return (
<ResponsiveBar
data={data}
keys={['sales']}
indexBy="month"
/>
)
}
При изменении salesData график автоматически
обновится.
Redux Toolkit содержит встроенную поддержку async logic через
createAsyncThunk.
import {
createSlice,
createAsyncThunk
} from '@reduxjs/toolkit'
export const fetchSalesData =
createAsyncThunk(
'charts/fetchSalesData',
async () => {
const response = await fetch('/api/sales')
return response.json()
}
)
const chartsSlice = createSlice({
name: 'charts',
initialState: {
salesData: [],
loading: false
},
extraReducers: builder => {
builder
.addCase(fetchSalesData.pending, state => {
state.loading = true
})
.addCase(fetchSalesData.fulfilled, (state, action) => {
state.loading = false
state.salesData = action.payload
})
}
})
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { fetchSalesData } from './chartsSlice'
export default function Dashboard() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchSalesData())
}, [])
return <Charts />
}
Большие наборы данных могут вызывать дорогостоящие рендеры.
Redux хорошо сочетается с reselect.
npm install reselect
import { createSelector } from 'reselect'
const salesData = state => state.charts.salesData
export const transformedData =
createSelector(
[salesData],
data => {
return data.map(item => ({
...item,
profit: item.sales - item.cost
}))
}
)
const data = useSelector(transformedData)
Трансформация выполнится только при изменении исходных данных.
Nivo может повторно рендерить SVG или Canvas-графики даже при минимальных изменениях объекта.
Плохой пример:
<ResponsivePie
data={[...data]}
/>
Каждый рендер создаёт новый массив.
Правильный вариант:
const stableData = useMemo(
() => data,
[data]
)
<ResponsivePie data={stableData} />
Redux Toolkit включает createEntityAdapter.
Это полезно при работе с большими аналитическими системами.
import {
createEntityAdapter,
createSlice
} from '@reduxjs/toolkit'
const adapter = createEntityAdapter()
const chartsSlice = createSlice({
name: 'charts',
initialState: adapter.getInitialState(),
reducers: {
addMany: adapter.addMany
}
})
Zustand — минималистичная библиотека управления состоянием.
Главные особенности:
Zustand особенно хорошо подходит для:
npm install zustand
import { create } from 'zustand'
export const useChartStore = create(set => ({
salesData: [],
setSalesData: data =>
set({
salesData: data
})
}))
import { ResponsiveLine } from '@nivo/line'
import { useChartStore } from './store'
export default function SalesChart() {
const data = useChartStore(
state => state.salesData
)
return (
<ResponsiveLine
data={data}
/>
)
}
const setSalesData =
useChartStore(
state => state.setSalesData
)
fetch('/api/data')
.then(r => r.json())
.then(data => {
setSalesData(data)
})
Одно из главных преимуществ Zustand — точечные подписки.
const filters = useChartStore(
state => state.filters
)
Компонент обновится только при изменении filters.
const salesData = useChartStore(
state => state.salesData
)
Можно комбинировать:
const chartState = useChartStore(
state => ({
data: state.salesData,
filters: state.filters
})
)
npm install zustand/middleware
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
set => ({
darkMode: false,
toggleTheme: () =>
set(state => ({
darkMode: !state.darkMode
}))
}),
{
name: 'dashboard-storage'
}
)
)
const useStore = create(set => ({
filters: {
year: 2025,
country: 'USA'
},
setFilter: (key, value) =>
set(state => ({
filters: {
...state.filters,
[key]: value
}
}))
}))
Zustand идеально подходит для coordinated views.
const selectedCountry =
useStore(state => state.selectedCountry)
Изменение состояния автоматически синхронизирует несколько графиков.
Zustand часто используют с WebSocket.
socket.onmess age = event => {
const data = JSON.parse(event.data)
useChartStore.setState({
salesData: data
})
}
Nivo мгновенно обновит визуализацию.
import { shallow } from 'zustand/shallow'
const state = useStore(
state => ({
filters: state.filters,
theme: state.theme
}),
shallow
)
Recoil основан на атомарной модели состояния.
Основные сущности:
Recoil особенно полезен при:
npm install recoil
import { RecoilRoot } from 'recoil'
root.render(
<RecoilRoot>
<App />
</RecoilRoot>
)
import { atom } from 'recoil'
export const salesState = atom({
key: 'salesState',
default: []
})
import { useRecoilValue } from 'recoil'
import { salesState } from './state'
const data = useRecoilValue(salesState)
<ResponsiveLine data={data} />
import { useSetRecoilState } from 'recoil'
const setSales =
useSetRecoilState(salesState)
fetch('/api/data')
.then(r => r.json())
.then(setSales)
import {
selector
} from 'recoil'
export const filteredSalesState =
selector({
key: 'filteredSalesState',
get: ({ get }) => {
const data = get(salesState)
return data.filter(
item => item.value > 100
)
}
})
const data = useRecoilValue(
filteredSalesState
)
Recoil автоматически отслеживает зависимости.
export const countryFilterState =
atom({
key: 'countryFilter',
default: 'USA'
})
export const chartDataState =
selector({
key: 'chartData',
get: ({ get }) => {
const country =
get(countryFilterState)
const sales =
get(salesState)
return sales.filter(
item => item.country === country
)
}
})
export const asyncChartData =
selector({
key: 'asyncChartData',
get: async () => {
const response =
await fetch('/api/chart')
return response.json()
}
})
Recoil тесно интегрируется с Suspense.
<Suspense fallback={<Loader />}>
<Chart />
</Suspense>
export const dashboardState =
atom({
key: 'dashboard',
default: {
sales: [],
revenue: [],
users: []
}
})
Полезно при динамических графиках.
import { selectorFamily } from 'recoil'
export const chartState =
selectorFamily({
key: 'chartState',
get: chartId => ({ get }) => {
const dashboard =
get(dashboardState)
return dashboard[chartId]
}
})
| Характеристика | Redux | Zustand | Recoil |
|---|---|---|---|
| Boilerplate | Высокий | Минимальный | Средний |
| Производительность | Высокая | Очень высокая | Высокая |
| Learning curve | Средний | Низкий | Средний |
| Derived state | Через selectors | Вручную | Встроено |
| Devtools | Отличные | Хорошие | Ограниченные |
| Async logic | Отличная | Простая | Встроенная |
| Масштабируемость | Очень высокая | Высокая | Высокая |
| Re-render optimization | Хорошая | Отличная | Отличная |
export default memo(SalesChart)
const processedData = useMemo(() => {
return transform(data)
}, [data])
const handleClick = useCallback(node => {
console.log(node)
}, [])
Плохой вариант:
<ResponsiveBar
margin={{
top: 50,
right: 20
}}
/>
Лучше:
const margin = {
top: 50,
right: 20
}
Nivo поддерживает:
Для больших потоков данных предпочтителен Canvas:
<ResponsiveLineCanvas />
const visibleData =
data.slice(start, end)
import debounce from 'lodash/debounce'
const updateChart =
debounce(data => {
setSalesData(data)
}, 300)
API
↓
State Manager
↓
Selectors
↓
Nivo Components
↓
SVG / Canvas
Рекомендуется разделять:
TanStack Query часто комбинируется с Zustand или Redux.
npm install @tanstack/react-query
const { data } = useQuery({
queryKey: ['sales'],
queryFn: fetchSales
})
const filters =
useStore(state => state.filters)
const filteredData =
useMemo(() => {
return applyFilters(data, filters)
}, [data, filters])
Типичная структура:
src/
├── store/
├── selectors/
├── charts/
├── dashboard/
├── hooks/
├── services/
└── api/
function SalesContainer() {
const data = useSelector(selectSales)
return <SalesChart data={data} />
}
function SalesChart({ data }) {
return (
<ResponsiveBar data={data} />
)
}
Большие графики могут содержать:
Подобные вычисления желательно мемоизировать.
const SalesChart =
lazy(() => import('./SalesChart'))
<ErrorBoundary>
<Chart />
</ErrorBoundary>
interface ChartState {
salesData: Sales[]
loading: boolean
}
const salesData = useSelector(
(state: RootState) =>
state.charts.salesData
)
interface Store {
salesData: Sales[]
setSalesData:
(data: Sales[]) => void
}
const salesState = atom<Sales[]>({
key: 'sales',
default: []
})