Explorez des patrons avancés pour l'API Context de React, incluant les composants composés, les contextes dynamiques, et des techniques d'optimisation pour la gestion d'états complexes.
Patrons Avancés de l'API Context de React pour la Gestion d'État
L'API Context de React fournit un mécanisme puissant pour partager l'état à travers votre application sans avoir à passer des props de haut en bas ("prop drilling"). Bien que son utilisation de base soit simple, exploiter tout son potentiel nécessite de comprendre des patrons avancés capables de gérer des scénarios complexes de gestion d'état. Cet article explore plusieurs de ces patrons, offrant des exemples pratiques et des perspectives concrètes pour améliorer votre développement React.
Comprendre les Limites de l'API Context de Base
Avant de plonger dans les patrons avancés, il est crucial de reconnaître les limites de l'API Context de base. Bien qu'adaptée à un état simple et globalement accessible, elle peut devenir difficile à gérer et inefficace pour les applications complexes avec un état qui change fréquemment. Chaque composant consommant un contexte se re-rend chaque fois que la valeur du contexte change, même si le composant ne dépend pas de la partie spécifique de l'état qui a été mise à jour. Cela peut entraîner des goulots d'étranglement en termes de performance.
Patron 1 : Composants Composés avec le Contexte
Le patron des Composants Composés (Compound Components) améliore l'API Context en créant une suite de composants liés qui partagent implicitement l'état et la logique via un contexte. Ce patron favorise la réutilisabilité et simplifie l'API pour les consommateurs. Cela permet d'encapsuler une logique complexe avec une implémentation simple.
Exemple : Un Composant d'Onglets
Illustrons cela avec un composant d'onglets. Au lieu de passer des props à travers plusieurs niveaux, les composants Tab
communiquent implicitement via un contexte partagé.
// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface TabContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabContext = createContext(undefined);
interface TabProviderProps {
children: ReactNode;
defaultTab: string;
}
export const TabProvider: React.FC = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
const value: TabContextType = {
activeTab,
setActiveTab,
};
return {children} ;
};
export const useTabContext = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabContext doit être utilisé à l\'intérieur d\'un TabProvider');
}
return context;
};
// TabList.js
import React, { ReactNode } from 'react';
interface TabListProps {
children: ReactNode;
}
export const TabList: React.FC = ({ children }) => {
return {children};
};
// Tab.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabProps {
label: string;
children: ReactNode;
}
export const Tab: React.FC = ({ label, children }) => {
const { activeTab, setActiveTab } = useTabContext();
const isActive = activeTab === label;
const handleClick = () => {
setActiveTab(label);
};
return (
);
};
// TabPanel.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabPanelProps {
label: string;
children: ReactNode;
}
export const TabPanel: React.FC = ({ label, children }) => {
const { activeTab } = useTabContext();
const isActive = activeTab === label;
return (
{isActive && children}
);
};
// Utilisation
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';
function App() {
return (
Onglet 1
Onglet 2
Onglet 3
Contenu de l'onglet 1
Contenu de l'onglet 2
Contenu de l'onglet 3
);
}
export default App;
Avantages :
- API simplifiée pour les consommateurs : Les utilisateurs n'ont qu'à se soucier de
Tab
,TabList
etTabPanel
. - Partage d'état implicite : Les composants accèdent et mettent à jour automatiquement l'état partagé.
- Réutilisabilité améliorée : Le composant
Tab
peut être facilement réutilisé dans différents contextes.
Patron 2 : Contextes Dynamiques
Dans certains scénarios, vous pourriez avoir besoin de différentes valeurs de contexte en fonction de la position du composant dans l'arborescence des composants ou d'autres facteurs dynamiques. Les contextes dynamiques vous permettent de créer et de fournir des valeurs de contexte qui varient en fonction de conditions spécifiques.
Exemple : Création de thèmes avec des contextes dynamiques
Considérez un système de thèmes où vous souhaitez fournir différents thèmes en fonction des préférences de l'utilisateur ou de la section de l'application dans laquelle il se trouve. Nous pouvons créer un exemple simplifié avec un thème clair et un thème sombre.
// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = () => {
setIsDarkTheme(!isDarkTheme);
};
const value: ThemeContextType = {
theme,
toggleTheme,
};
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
// Utilisation
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
Ceci est un composant à thème.
);
}
function App() {
return (
);
}
export default App;
Dans cet exemple, le ThemeProvider
détermine dynamiquement le thème en fonction de l'état isDarkTheme
. Les composants utilisant le hook useTheme
se re-rendront automatiquement lorsque le thème changera.
Patron 3 : Contexte avec useReducer pour un État Complexe
Pour gérer une logique d'état complexe, combiner l'API Context avec useReducer
est une excellente approche. useReducer
fournit une manière structurée de mettre à jour l'état en fonction d'actions, et l'API Context vous permet de partager cet état et la fonction de dispatch à travers votre application.
Exemple : Une Simple Liste de Tâches
// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
interface TodoContextType {
state: TodoState;
dispatch: React.Dispatch;
}
const initialState: TodoState = {
todos: [],
};
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
const TodoContext = createContext(undefined);
interface TodoProviderProps {
children: ReactNode;
}
export const TodoProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, initialState);
const value: TodoContextType = {
state,
dispatch,
};
return {children} ;
};
export const useTodo = () => {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodo doit être utilisé à l\'intérieur d\'un TodoProvider');
}
return context;
};
// Utilisation
import { useTodo, TodoProvider } from './TodoContext';
function TodoList() {
const { state, dispatch } = useTodo();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function AddTodo() {
const { dispatch } = useTodo();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
Ce patron centralise la logique de gestion de l'état au sein du reducer, ce qui la rend plus facile à comprendre et à tester. Les composants peuvent dispatcher des actions pour mettre à jour l'état sans avoir à gérer l'état directement.
Patron 4 : Mises à Jour de Contexte Optimisées avec `useMemo` et `useCallback`
Comme mentionné précédemment, une considération de performance clé avec l'API Context est les re-renders inutiles. L'utilisation de useMemo
et useCallback
peut empêcher ces re-renders en s'assurant que seules les parties nécessaires de la valeur du contexte sont mises à jour, et que les références de fonction restent stables.
Exemple : Optimisation d'un Contexte de Thème
// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = useCallback(() => {
setIsDarkTheme(!isDarkTheme);
}, [isDarkTheme]);
const value: ThemeContextType = useMemo(() => ({
theme,
toggleTheme,
}), [theme, toggleTheme]);
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
Explication :
useCallback
mémorise la fonctiontoggleTheme
. Cela garantit que la référence de la fonction ne change que lorsqueisDarkTheme
change, empêchant les re-renders inutiles des composants qui ne dépendent que de la fonctiontoggleTheme
.useMemo
mémorise la valeur du contexte. Cela garantit que la valeur du contexte ne change que lorsque letheme
ou la fonctiontoggleTheme
changent, empêchant ainsi davantage de re-renders inutiles.
Sans useCallback
, la fonction toggleTheme
serait recréée à chaque rendu du ThemeProvider
, ce qui ferait changer la value
et déclencherait des re-renders dans tous les composants consommateurs, même si le thème lui-même n'avait pas changé. useMemo
garantit qu'une nouvelle value
n'est créée que lorsque ses dépendances (theme
ou toggleTheme
) changent.
Patron 5 : Sélecteurs de Contexte
Les sélecteurs de contexte permettent aux composants de s'abonner uniquement à des parties spécifiques de la valeur du contexte. Cela évite les re-renders inutiles lorsque d'autres parties du contexte changent. Des bibliothèques comme `use-context-selector` ou des implémentations personnalisées peuvent être utilisées pour y parvenir.
Exemple d'Utilisation d'un Sélecteur de Contexte Personnalisé
// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';
function useCustomContextSelector(
context: React.Context,
selector: (value: T) => S
): S {
const value = useContext(context);
const [selected, setSelected] = useState(() => selector(value));
const latestSelector = useRef(selector);
latestSelector.current = selector;
useEffect(() => {
let didUnmount = false;
let lastSelected = selected;
const subscription = () => {
if (didUnmount) {
return;
}
const nextSelected = latestSelector.current(value);
if (!Object.is(lastSelected, nextSelected)) {
lastSelected = nextSelected;
setSelected(nextSelected);
}
};
// Normalement, vous vous abonneriez aux changements de contexte ici. Comme c'est un exemple simplifié,
// nous appellerons simplement la souscription immédiatement pour initialiser.
subscription();
return () => {
didUnmount = true;
// Se désabonner des changements de contexte ici, si applicable.
};
}, [value]); // Réexécute l'effet chaque fois que la valeur du contexte change
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (Simplifié pour la brièveté)
import React, { createContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
setTheme: (newTheme: Theme) => void;
}
const ThemeContext = createContext(undefined);
interface ThemeProviderProps {
children: ReactNode;
initialTheme: Theme;
}
export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
const [theme, setTheme] = useState(initialTheme);
const value: ThemeContextType = {
theme,
setTheme
};
return {children} ;
};
export const useThemeContext = () => {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error("useThemeContext doit être utilisé à l'intérieur d'un ThemeProvider");
}
return context;
};
export default ThemeContext;
// Utilisation
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';
function BackgroundComponent() {
const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
return Arrière-plan;
}
function ColorComponent() {
const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color);
return Couleur;
}
function App() {
const { theme, setTheme } = useThemeContext();
const toggleTheme = () => {
setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' });
};
return (
);
}
export default App;
Dans cet exemple, BackgroundComponent
ne se re-rend que lorsque la propriété background
du thème change, et ColorComponent
ne se re-rend que lorsque la propriété color
change. Cela évite les re-renders inutiles lorsque la valeur entière du contexte change.
Patron 6 : Séparer les Actions de l'État
Pour les applications plus volumineuses, envisagez de séparer la valeur du contexte en deux contextes distincts : un pour l'état et un autre pour les actions (fonctions de dispatch). Cela peut améliorer l'organisation du code et la testabilité.
Exemple : Liste de Tâches avec des Contextes d'État et d'Action Séparés
// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
const initialState: TodoState = {
todos: [],
};
const TodoStateContext = createContext(initialState);
interface TodoStateProviderProps {
children: ReactNode;
}
export const TodoStateProvider: React.FC = ({ children }) => {
const [state] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoState = () => {
return useContext(TodoStateContext);
};
// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
const TodoActionContext = createContext | undefined>(undefined);
interface TodoActionProviderProps {
children: ReactNode;
}
export const TodoActionProvider: React.FC = ({children}) => {
const [, dispatch] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoDispatch = () => {
const dispatch = useContext(TodoActionContext);
if (!dispatch) {
throw new Error('useTodoDispatch doit être utilisé à l\'intérieur d\'un TodoActionProvider');
}
return dispatch;
};
// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
// Utilisation
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';
function TodoList() {
const state = useTodoState();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function TodoActions({ todo }) {
const dispatch = useTodoDispatch();
return (
<>
>
);
}
function AddTodo() {
const dispatch = useTodoDispatch();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
Cette séparation permet aux composants de ne s'abonner qu'au contexte dont ils ont besoin, réduisant ainsi les re-renders inutiles. Elle facilite également les tests unitaires du reducer et de chaque composant de manière isolée. De plus, l'ordre d'imbrication des fournisseurs est important. Le ActionProvider
doit englober le StateProvider
.
Meilleures Pratiques et Considérations
- Le contexte ne doit pas remplacer toutes les bibliothèques de gestion d'état : Pour les applications très grandes et complexes, des bibliothèques de gestion d'état dédiées comme Redux ou Zustand pourraient être un meilleur choix.
- Évitez la sur-contextualisation : Chaque morceau d'état n'a pas besoin d'être dans un contexte. Utilisez le contexte judicieusement pour un état véritablement global ou largement partagé.
- Tests de performance : Mesurez toujours l'impact sur les performances de votre utilisation du contexte, en particulier lorsque vous traitez un état qui se met à jour fréquemment.
- Fractionnement du code (Code Splitting) : Lorsque vous utilisez l'API Context, envisagez de fractionner votre application en plus petits morceaux. C'est particulièrement important lorsqu'un petit changement d'état provoque le re-render d'une grande partie de l'application.
Conclusion
L'API Context de React est un outil polyvalent pour la gestion d'état. En comprenant et en appliquant ces patrons avancés, vous pouvez gérer efficacement un état complexe, optimiser les performances et créer des applications React plus maintenables et évolutives. N'oubliez pas de choisir le bon patron pour vos besoins spécifiques et de considérer attentivement les implications de performance de votre utilisation du contexte.
À mesure que React évolue, les meilleures pratiques entourant l'API Context évolueront également. Rester informé des nouvelles techniques et bibliothèques vous assurera d'être équipé pour relever les défis de la gestion d'état dans le développement web moderne. Envisagez d'explorer les patrons émergents comme l'utilisation du contexte avec des signaux pour une réactivité encore plus fine.