Recoil a Zustand 🚀
Ukazovali jsme si, jak spravovat a sdílet stav mezi komponentami v Reactu pomocí useState a Context API.
Teď je čas podívat se na moderní state management knihovny, které nám mohou práci ještě více usnadnit.
V této části se podíváme na moderní state management knihovny, které nabízejí jednodušší a výkonnější alternativy k Reduxu a Context API.
🎯 Proč moderní state management?
Problémy s tradičními řešeními:
- ❌ Redux - složitý setup, boilerplate kód
- ❌ Context API - performance problémy, re-renders
- ❌ useState - prop drilling, složitá logika
Výhody moderních knihoven:
- ✅ Centralizace stavu - snadné sdílení stavu mezi komponentami a umístění logiky mimo komponenty
- ✅ Výkon - optimalizované překreslování (
re-render) - ✅ Jednoduché nastavení a zprovoznění - minimum konfigurace
- ✅ TypeScript - výborná podpora typování
- ✅ Nástroje pro vývojáře - debugging nástroje - ukazují, co se v aplikaci děje a tím usnadní ladění (
debug)
🚀 Recoil
Recoil je state management knihovna od Facebooku, která používá atom-based přístup.
Instalace:
npm install recoil
Základní setup:
// App.jsx
import React from 'react';
import { RecoilRoot } from 'recoil';
import EmployeeList from './components/EmployeeList';
import EmployeeDetail from './components/EmployeeDetail';
const App = () => {
return (
<RecoilRoot>
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-900 mb-8">
Seznam zaměstnanců
</h1>
<EmployeeList />
<EmployeeDetail />
</div>
</div>
</RecoilRoot>
);
};
export default App;
Atoms (stav):
// atoms/employeeAtoms.js
import { atom } from 'recoil';
// Základní atoms
export const employeesAtom = atom({
key: 'employeesState',
default: []
});
export const selectedEmployeeAtom = atom({
key: 'selectedEmployeeState',
default: null
});
export const filterAtom = atom({
key: 'filterState',
default: 'all'
});
export const loadingAtom = atom({
key: 'loadingState',
default: false
});
export const errorAtom = atom({
key: 'errorState',
default: null
});
Selectors (vypočítané hodnoty):
// selectors/employeeSelectors.js
import { selector } from 'recoil';
import { employeesAtom, filterAtom } from '../atoms/employeeAtoms';
export const filteredEmployeesSelector = selector({
key: 'filteredEmployeesState',
get: ({ get }) => {
const employees = get(employeesAtom);
const filter = get(filterAtom);
if (filter === 'all') {
return employees;
}
return employees.filter(emp => emp.department === filter);
}
});
export const employeeStatsSelector = selector({
key: 'employeeStatsState',
get: ({ get }) => {
const employees = get(employeesAtom);
return {
total: employees.length,
byDepartment: employees.reduce((acc, emp) => {
acc[emp.department] = (acc[emp.department] || 0) + 1;
return acc;
}, {}),
averageSalary: employees.reduce((sum, emp) => sum + emp.salary, 0) / employees.length
};
}
});
Použití v komponentách:
// components/EmployeeList.jsx
import React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { employeesAtom, filterAtom, filteredEmployeesSelector } from '../atoms/employeeAtoms';
import { employeeStatsSelector } from '../selectors/employeeSelectors';
import EmployeeCard from './EmployeeCard';
import FilterBar from './FilterBar';
const EmployeeList = () => {
const [filter, setFilter] = useRecoilState(filterAtom);
const filteredEmployees = useRecoilValue(filteredEmployeesSelector);
const stats = useRecoilValue(employeeStatsSelector);
return (
<div className="w-full">
<div className="mb-4 p-4 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold mb-2">Statistiky</h3>
<p>Celkem zaměstnanců: {stats.total}</p>
<p>Průměrná mzda: {stats.averageSalary.toFixed(0)} Kč</p>
</div>
<FilterBar
currentFilter={filter}
onFilterChange={setFilter}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredEmployees.map(employee => (
<EmployeeCard
key={employee.id}
employee={employee}
/>
))}
</div>
</div>
);
};
export default EmployeeList;
Async data loading:
// selectors/asyncSelectors.js
import { selector } from 'recoil';
import apiService from '../services/api';
export const employeesQuery = selector({
key: 'employeesQuery',
get: async () => {
try {
const employees = await apiService.fetchEmployees();
return employees;
} catch (error) {
throw error;
}
}
});
// Použití v komponentě
const EmployeeList = () => {
const employees = useRecoilValue(employeesQuery);
if (employees.length === 0) {
return <div>Načítání...</div>;
}
return (
<div>
{employees.map(employee => (
<EmployeeCard key={employee.id} employee={employee} />
))}
</div>
);
};
🎨 Zustand
Zustand je minimalistická state management knihovna s jednoduchým API.
Instalace:
npm install zustand
Základní store:
// stores/employeeStore.js
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import apiService from '../services/api';
export const useEmployeeStore = create(
devtools(
(set, get) => ({
// State
employees: [],
selectedEmployee: null,
filter: 'all',
loading: false,
error: null,
// Actions
setEmployees: (employees) => set({ employees }),
setSelectedEmployee: (employee) => set({ selectedEmployee: employee }),
setFilter: (filter) => set({ filter }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
// Async actions
loadEmployees: async () => {
try {
set({ loading: true, error: null });
const employees = await apiService.fetchEmployees();
set({ employees, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
addEmployee: async (employeeData) => {
try {
set({ loading: true });
const newEmployee = await apiService.createEmployee(employeeData);
set(state => ({
employees: [...state.employees, newEmployee],
loading: false
}));
} catch (error) {
set({ error: error.message, loading: false });
}
},
updateEmployee: async (id, employeeData) => {
try {
set({ loading: true });
const updatedEmployee = await apiService.updateEmployee(id, employeeData);
set(state => ({
employees: state.employees.map(emp =>
emp.id === id ? updatedEmployee : emp
),
selectedEmployee: state.selectedEmployee?.id === id
? updatedEmployee
: state.selectedEmployee,
loading: false
}));
} catch (error) {
set({ error: error.message, loading: false });
}
},
deleteEmployee: async (id) => {
try {
set({ loading: true });
await apiService.deleteEmployee(id);
set(state => ({
employees: state.employees.filter(emp => emp.id !== id),
selectedEmployee: state.selectedEmployee?.id === id
? null
: state.selectedEmployee,
loading: false
}));
} catch (error) {
set({ error: error.message, loading: false });
}
},
// Computed values
getFilteredEmployees: () => {
const { employees, filter } = get();
return filter === 'all'
? employees
: employees.filter(emp => emp.department === filter);
},
getEmployeeStats: () => {
const { employees } = get();
return {
total: employees.length,
byDepartment: employees.reduce((acc, emp) => {
acc[emp.department] = (acc[emp.department] || 0) + 1;
return acc;
}, {}),
averageSalary: employees.reduce((sum, emp) => sum + emp.salary, 0) / employees.length
};
}
}),
{
name: 'employee-store', // název pro DevTools
}
)
);
Použití v komponentách:
// components/EmployeeList.jsx
import React, { useEffect } from 'react';
import { useEmployeeStore } from '../stores/employeeStore';
import EmployeeCard from './EmployeeCard';
import FilterBar from './FilterBar';
const EmployeeList = () => {
const {
employees,
filter,
loading,
error,
setFilter,
loadEmployees,
getFilteredEmployees,
getEmployeeStats
} = useEmployeeStore();
const filteredEmployees = getFilteredEmployees();
const stats = getEmployeeStats();
useEffect(() => {
loadEmployees();
}, [loadEmployees]);
if (loading) {
return <div className="text-center py-8">Načítání...</div>;
}
if (error) {
return <div className="text-center py-8 text-red-600">Chyba: {error}</div>;
}
return (
<div className="w-full">
<div className="mb-4 p-4 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold mb-2">Statistiky</h3>
<p>Celkem zaměstnanců: {stats.total}</p>
<p>Průměrná mzda: {stats.averageSalary.toFixed(0)} Kč</p>
</div>
<FilterBar
currentFilter={filter}
onFilterChange={setFilter}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredEmployees.map(employee => (
<EmployeeCard
key={employee.id}
employee={employee}
/>
))}
</div>
</div>
);
};
export default EmployeeList;
Selektory pro optimalizaci:
// components/EmployeeCard.jsx
import React from 'react';
import { useEmployeeStore } from '../stores/employeeStore';
const EmployeeCard = ({ employee }) => {
// Pouze selektory, které komponenta skutečně potřebuje
const setSelectedEmployee = useEmployeeStore(state => state.setSelectedEmployee);
const handleClick = () => {
setSelectedEmployee(employee);
};
return (
<div
className="bg-white rounded-lg p-4 shadow-md cursor-pointer hover:shadow-lg transition-shadow"
onClick={handleClick}
>
<h3 className="text-lg font-semibold">{employee.name}</h3>
<p className="text-gray-600">{employee.position}</p>
<span className="text-sm text-blue-600">{employee.department}</span>
</div>
);
};
export default EmployeeCard;
🔄 Porovnání knihoven
| Knihovna | Výhody | Nevýhody | Kdy použít |
|---|---|---|---|
| Recoil | ✅ Atom-based, selektory, async | ❌ Novější, menší komunita | Komplexní aplikace s atomovým stavem |
| Zustand | ✅ Jednoduché, výkonné, TypeScript | ❌ Méně funkcí než Redux | Střední až velké aplikace |
| Redux | ✅ Zralé, velká komunita, DevTools | ❌ Složitý setup, boilerplate | Velké aplikace, tým zkušený s Reduxem |
| Context API | ✅ Vestavěné, jednoduché | ❌ Performance problémy | Malé aplikace, jednoduchý stav |
📋 Praktické cvičení
- Implementujte Recoil store pro zaměstnance
- Vytvořte Zustand store se stejnou funkcionalitou
- Porovnejte výkon obou řešení
- Přidejte TypeScript podporu
- Implementujte DevTools pro debugging
🚀 Tipy a triky
1. Zustand s TypeScript:
interface EmployeeState {
employees: Employee[];
selectedEmployee: Employee | null;
filter: string;
loading: boolean;
error: string | null;
}
interface EmployeeActions {
setEmployees: (employees: Employee[]) => void;
setSelectedEmployee: (employee: Employee | null) => void;
setFilter: (filter: string) => void;
loadEmployees: () => Promise<void>;
}
export const useEmployeeStore = create<EmployeeState & EmployeeActions>()(
devtools(
(set, get) => ({
// Implementation...
})
)
);
2. Recoil s TypeScript:
export const employeesAtom = atom<Employee[]>({
key: 'employeesState',
default: []
});
export const filteredEmployeesSelector = selector<Employee[]>({
key: 'filteredEmployeesState',
get: ({ get }) => {
const employees = get(employeesAtom);
const filter = get(filterAtom);
return filter === 'all' ? employees : employees.filter(emp => emp.department === filter);
}
});
3. Performance optimalizace:
// Zustand - selektory pro minimalizaci re-renderů
const EmployeeCard = ({ employee }) => {
const setSelectedEmployee = useEmployeeStore(state => state.setSelectedEmployee);
// Komponenta se re-renderuje pouze při změně setSelectedEmployee
};
// Recoil - atomFamily pro dynamické atomy
export const employeeAtom = atomFamily({
key: 'employeeState',
default: (id) => null
});
Moderní state management knihovny nabízejí jednodušší a výkonnější alternativy k tradičním řešením. V příští části se podíváme na Redux! 🚀