Chapitre 2 : Gestion d'état
SmartCommon propose plusieurs hooks pour gérer l'état selon les besoins :
- useGlobalStates : état partagé entre composants avec persistance
- useStates : état local d'un composant
- useForm : état spécialisé pour les formulaires
useGlobalStates
État global accessible partout, avec persistance automatique.
Syntaxe de base
import { useGlobalStates } from '@cap-rel/smartcommon';
function MyComponent() {
const gst = useGlobalStates();
return (
<div>
<p>Utilisateur : {gst.get('session')?.user?.name}</p>
<p>Langue : {gst.get('settings')?.lng}</p>
</div>
);
}
Configuration de la persistance
Dans appConfig.js :
export const config = {
storage: {
local: ["session", "settings"] // Persisté en localStorage
},
globalState: {
reducers: {
session: null,
settings: { lng: "fr", theme: "light" },
cart: { items: [], total: 0 }
}
}
};
Lecture et écriture
function SettingsPage() {
const gst = useGlobalStates();
const settings = gst.get('settings');
const changeLanguage = (lng) => {
gst.local.set('settings', { ...settings, lng });
};
const changeTheme = (theme) => {
gst.local.set('settings', { ...settings, theme });
};
return (
<div>
<select
value={settings?.lng}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value="fr">Français</option>
<option value="en">English</option>
</select>
<button onClick={() => changeTheme('light')}>Clair</button>
<button onClick={() => changeTheme('dark')}>Sombre</button>
</div>
);
}
Exemple : Panier d'achat
function useCart() {
const gst = useGlobalStates();
const cart = gst.get('cart');
const addItem = (product) => {
const existing = cart.items.find(i => i.id === product.id);
let newItems;
if (existing) {
newItems = cart.items.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
} else {
newItems = [...cart.items, { ...product, quantity: 1 }];
}
const total = newItems.reduce(
(sum, i) => sum + i.price * i.quantity,
0
);
gst.set('cart', { items: newItems, total });
};
const removeItem = (productId) => {
const newItems = cart.items.filter(i => i.id !== productId);
const total = newItems.reduce(
(sum, i) => sum + i.price * i.quantity,
0
);
gst.set('cart', { items: newItems, total });
};
const clearCart = () => {
gst.set('cart', { items: [], total: 0 });
};
return { cart, addItem, removeItem, clearCart };
}
// Utilisation
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div>
<h3>{product.label}</h3>
<p>{product.price} €</p>
<button onClick={() => addItem(product)}>
Ajouter au panier
</button>
</div>
);
}
function CartIcon() {
const { cart } = useCart();
const itemCount = cart.items.reduce((sum, i) => sum + i.quantity, 0);
return <span>🛒 {itemCount}</span>;
}
useStates
État local avec notation par chemin (path notation).
Syntaxe de base
import { useStates } from '@cap-rel/smartcommon';
function MyComponent() {
const st = useStates({
initialStates: {
count: 0,
user: { name: '', email: '' },
items: [],
loading: false
},
debug: true // Affiche les changements en console
});
return (
<div>
<p>Count: {st.get('count')}</p>
<button onClick={() => st.set('count', st.get('count') + 1)}>
+1
</button>
</div>
);
}
Méthodes disponibles
| Méthode | Description |
|---|---|
| st.get(path) | Lire une valeur |
| st.set(path, value) | Écrire une valeur |
| st.unset(path) | Supprimer une valeur |
| st.values | Objet contenant toutes les valeurs |
Path notation
const st = useStates({
initialStates: {
user: { name: '', address: { city: '' } },
items: []
}
});
// Lecture
st.get('user'); // { name: '', address: { city: '' } }
st.get('user.name'); // ''
st.get('user.address.city'); // ''
st.get('items'); // []
st.get('items[0]'); // undefined
// Écriture
st.set('user.name', 'Jean');
st.set('user.address.city', 'Paris');
// Écriture avec fonction
st.set('count', prev => prev + 1);
// Manipulation de tableaux
st.set('items[]', { id: 1 }); // Push
st.set('items[0].name', 'test'); // Modifier index
st.unset('items[0]'); // Supprimer index
Exemple : Page de détail
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Page, Block, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates } from '@cap-rel/smartcommon';
export const ProductDetailPage = () => {
const { id } = useParams();
const api = useApi();
const st = useStates({
initialStates: {
product: null,
loading: true,
error: null,
isEditing: false
}
});
useEffect(() => {
loadProduct();
}, [id]);
const loadProduct = async () => {
st.set('loading', true);
st.set('error', null);
try {
const data = await api.private.get(`products/${id}`).json();
st.set('product', data);
} catch (err) {
st.set('error', err.message);
} finally {
st.set('loading', false);
}
};
if (st.get('loading')) {
return <Page><Spinner /></Page>;
}
if (st.get('error')) {
return <Page><Block>Erreur : {st.get('error')}</Block></Page>;
}
const product = st.get('product');
return (
<Page title={product.label}>
<Block>
<p>Prix : {product.price} €</p>
<p>Stock : {product.stock}</p>
</Block>
</Page>
);
};
useForm
Hook spécialisé pour les formulaires avec validation.
Syntaxe de base
import { useForm } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
function LoginForm() {
const form = useForm({ defaultValues: { email: '', password: '' } });
const handleSubmit = async (data) => {
console.log('Données validées:', data);
};
return (
<Form form={form} onSubmit={handleSubmit}>
<Input name="email" label="Email" type="email" />
<Input name="password" label="Mot de passe" type="password" />
<Button type="submit" loading={form.isFormSubmitting}>
Connexion
</Button>
</Form>
);
}
Avec valeurs initiales
function EditProductForm({ product }) {
const form = useForm({
defaultValues: {
label: product.label,
price: product.price,
description: product.description
}
});
const handleSubmit = async (data) => {
await api.private.put(`products/${product.id}`, { json: data });
};
return (
<Form form={form} onSubmit={handleSubmit}>
<Input name="label" label="Nom" />
<Input name="price" label="Prix" type="number" />
<Textarea name="description" label="Description" />
<Button type="submit">Enregistrer</Button>
</Form>
);
}
Validation manuelle avec setField
// useForm ne fait pas de validation automatique.
// Utilisez setField pour gérer les erreurs manuellement :
form.setField({
name: 'email',
value: inputValue,
errors: {
required: { condition: !inputValue },
format: { condition: inputValue && !isValidEmail(inputValue) }
}
});
// Vérifier les erreurs
const hasError = form.get('errors.email.required'); // true | false
Comparaison des hooks d'état
| Hook | Portée | Persistance | Cas d'usage |
|---|---|---|---|
| useGlobalStates | Application | localStorage | Session, préférences, panier |
| useStates | Composant | Non | État de page, chargement |
| useForm | Composant | Non | Formulaires avec validation |
| useState (React) | Composant | Non | État simple |
Bonnes pratiques
1. Choisir le bon hook
// Session utilisateur -> useGlobalStates
const gst = useGlobalStates();
const session = gst.get('session');
// État de chargement d'une page -> useStates
const st = useStates({ initialStates: { loading: true, data: null } });
// Formulaire -> useForm
const form = useForm({ defaultValues: { name: '', email: '' } });
// Toggle simple -> useState
const [isOpen, setIsOpen] = useState(false);
2. Organiser l'état global
// appConfig.js
globalState: {
reducers: {
// Auth
session: null,
// Préférences utilisateur
settings: { lng: 'fr', theme: 'light' },
// Données métier globales
cart: { items: [], total: 0 },
// Cache
categories: []
}
}
useConfirm
Hook pour afficher des dialogues de confirmation et d'alerte modaux.
Import
import { useConfirm } from '@cap-rel/smartcommon';
Fonctions retournées
Le hook retourne un objet avec deux fonctions :
const { confirm, alert } = useConfirm();
- confirm : affiche un dialogue avec boutons Confirmer / Annuler, retourne
trueoufalse - alert : affiche un dialogue avec un seul bouton OK, retourne toujours
true
Utilisation de confirm
function DeleteButton({ item, onDelete }) {
const { confirm } = useConfirm();
const handleDelete = async () => {
const confirmed = await confirm({
type: 'delete',
title: 'Supprimer cet élément ?',
message: `Voulez-vous vraiment supprimer "${item.name}" ?`,
detail: item.ref,
confirmText: 'Supprimer',
cancelText: 'Annuler'
});
if (confirmed) {
onDelete(item.id);
}
};
return <button onClick={handleDelete}>Supprimer</button>;
}
Utilisation de alert
function SaveButton({ onSave }) {
const { alert } = useConfirm();
const handleSave = async () => {
try {
await onSave();
await alert({
type: 'info',
title: 'Succès',
message: 'Les données ont été enregistrées.'
});
} catch (err) {
await alert({
type: 'warning',
title: 'Erreur',
message: err.message
});
}
};
return <button onClick={handleSave}>Enregistrer</button>;
}
Options
| Option | Type | Description |
|---|---|---|
| type | string | Type de dialogue : 'danger', 'delete', 'warning', 'info' |
| title | string | Titre du dialogue |
| message | string | Message de confirmation |
| detail | string | Texte complémentaire affiché dans un encadré gris |
| confirmText | string | Texte du bouton de confirmation |
| cancelText | string | Texte du bouton d'annulation |
Types et icônes
| Type | Icône | Couleur du bouton |
|---|---|---|
| danger | Corbeille (rouge) | Rouge |
| delete | Corbeille (rouge) | Rouge |
| warning | Triangle exclamation (orange) | Orange |
| info | Info (bleu) | Bleu |
| (autre) | Point d'interrogation (gris) | Bleu (défaut) |
Prérequis
Le composant ConfirmProvider doit envelopper l'application :
import { ConfirmProvider } from '@cap-rel/smartcommon';
function App() {
return (
<ConfirmProvider labels={{ cancel: 'Annuler', confirm: 'OK' }}>
<MyApp />
</ConfirmProvider>
);
}
La prop labels permet de définir les textes par défaut des boutons. Si non fournie, les valeurs sont "Cancel" et "OK".
usePWAUpdate
Hook pour détecter et gérer les mises à jour de l'application PWA.
Import
import { usePWAUpdate } from '@cap-rel/smartcommon';
Utilisation
function UpdateBanner() {
const {
updateAvailable,
updateActivated,
checkForUpdates,
applyUpdate
} = usePWAUpdate({
checkInterval: 300000 // Vérifier toutes les 5 min
});
if (!updateAvailable) return null;
return (
<div className="bg-blue-500 text-white p-4">
<p>Une mise à jour est disponible</p>
<button onClick={applyUpdate}>
Mettre à jour maintenant
</button>
</div>
);
}
Avec rechargement automatique
function App() {
usePWAUpdate({
autoReload: true,
onUpdateAvailable: () => {
console.log('Mise à jour disponible');
},
onUpdateActivated: () => {
console.log('Mise à jour activée');
}
});
return <MyApp />;
}
Options
| Option | Type | Défaut | Description |
|---|---|---|---|
| autoReload | boolean | false | Recharger automatiquement après mise à jour |
| checkInterval | number | 0 | Intervalle de vérification en ms (0 = désactivé) |
| onUpdateAvailable | function | - | Callback quand une mise à jour est disponible |
| onUpdateActivated | function | - | Callback quand la mise à jour est activée |
Valeurs retournées
| Propriété | Type | Description |
|---|---|---|
| updateAvailable | boolean | Mise à jour en attente |
| updateActivated | boolean | Mise à jour activée |
| registration | object | ServiceWorkerRegistration |
| checkForUpdates | function | Vérifier manuellement |
| applyUpdate | function | Appliquer la mise à jour |
| reloadPage | function | Recharger la page |
UpdatePrompt
Composant UI prêt à l'emploi qui encapsule usePWAUpdate et affiche une notification quand une mise à jour est disponible.
Import
import { UpdatePrompt } from '@cap-rel/smartcommon';
Variantes d'affichage
Trois variantes sont disponibles :
- toast (défaut) : notification discrète via react-hot-toast
- banner : bandeau fixe en haut ou en bas de l'écran
- modal : dialogue modal centré
Utilisation directe
function App() {
return (
<div>
<MyApp />
<UpdatePrompt
variant="banner"
position="bottom"
checkInterval={300000}
labels={{
title: 'Nouvelle version',
message: 'Une mise à jour est disponible.',
reloadButton: 'Rafraîchir',
dismissButton: 'Plus tard'
}}
/>
</div>
);
}
Via le Provider
Le Provider de SmartCommon accepte une prop pwaUpdate qui intègre automatiquement UpdatePrompt :
import { Provider } from '@cap-rel/smartcommon';
import { config } from './appConfig';
export const App = () => (
<Provider
config={config}
pwaUpdate={{
variant: 'toast',
checkInterval: 300000
}}
>
<Router />
</Provider>
);
Props
| Prop | Type | Défaut | Description |
|---|---|---|---|
| variant | string | "toast" |
"toast", "banner" ou "modal" |
| position | string | "bottom" |
Position du banner : "top" ou "bottom" |
| autoReload | boolean | false |
Recharger automatiquement après activation |
| checkInterval | number | 0 |
Intervalle de vérification en ms (0 = désactivé) |
| labels | object | - | Textes personnalisés (voir ci-dessous) |
| onUpdateAvailable | function | - | Callback quand une mise à jour est détectée |
| onUpdateActivated | function | - | Callback quand la mise à jour est activée |
Labels par défaut
| Clé | Valeur par défaut |
|---|---|
| title | "Mise à jour disponible" |
| message | "Une nouvelle version est disponible." |
| reloadButton | "Rafraîchir" |
| dismissButton | "Plus tard" |
Points clés à retenir
- useGlobalStates pour les données partagées et persistées
- useStates pour l'état local avec path notation
- useForm pour les formulaires avec validation manuelle
- useConfirm pour les dialogues de confirmation et d'alerte
- usePWAUpdate pour détecter les mises à jour PWA
- UpdatePrompt pour afficher une UI de mise à jour prête à l'emploi
- Path notation :
user.address.city,items[0],items[] - Configurer storage.local pour la persistance
← Chapitre précédent | Retour au module | Chapitre suivant : Utilitaires ->