Table des matières

Chapitre 2 : Gestion d'état

SmartCommon propose plusieurs hooks pour gérer l'état selon les besoins :

useGlobalStates

État global accessible partout, avec persistance automatique.

Syntaxe de base

snippet.javascript
import { useGlobalStates } from '@cap-rel/smartcommon';
 
function MyComponent() {
    const [session, setSession] = useGlobalStates('session');
    const [settings, setSettings] = useGlobalStates('settings');
 
    return (
        <div>
            <p>Utilisateur : {session?.user?.name}</p>
            <p>Langue : {settings?.lng}</p>
        </div>
    );
}

Configuration de la persistance

Dans appConfig.js :

snippet.javascript
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

snippet.javascript
function SettingsPage() {
    const [settings, setSettings] = useGlobalStates('settings');
 
    const changeLanguage = (lng) => {
        setSettings({ ...settings, lng });
    };
 
    const changeTheme = (theme) => {
        setSettings({ ...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

snippet.javascript
function useCart() {
    const [cart, setCart] = useGlobalStates('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
        );
 
        setCart({ 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
        );
        setCart({ items: newItems, total });
    };
 
    const clearCart = () => {
        setCart({ 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

snippet.javascript
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

snippet.javascript
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

snippet.javascript
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

snippet.javascript
import { useForm } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';
 
const schema = z.object({
    email: z.string().email('Email invalide'),
    password: z.string().min(6, 'Minimum 6 caractères')
});
 
function LoginForm() {
    const form = useForm({ schema });
 
    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.formState.isSubmitting}>
                Connexion
            </Button>
        </Form>
    );
}

Avec valeurs initiales

snippet.javascript
function EditProductForm({ product }) {
    const form = useForm({
        schema: productSchema,
        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 Zod

snippet.javascript
import { z } from 'zod';
 
const productSchema = z.object({
    label: z.string()
        .min(2, 'Minimum 2 caractères')
        .max(100, 'Maximum 100 caractères'),
 
    price: z.number()
        .min(0, 'Le prix doit être positif')
        .max(99999, 'Prix trop élevé'),
 
    description: z.string().optional(),
 
    category: z.enum(['electronics', 'clothing', 'food'], {
        errorMap: () => ({ message: 'Catégorie invalide' })
    }),
 
    stock: z.number().int('Doit être un entier').min(0),
 
    isActive: z.boolean().default(true)
});
 
// Validation conditionnelle
const orderSchema = z.object({
    deliveryType: z.enum(['standard', 'express']),
    expressDate: z.string().optional()
}).refine(
    data => data.deliveryType !== 'express' || data.expressDate,
    { message: 'Date requise pour livraison express', path: ['expressDate'] }
);

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

snippet.javascript
// Session utilisateur → useGlobalStates
const [session] = useGlobalStates('session');
 
// État de chargement d'une page → useStates
const st = useStates({ initialStates: { loading: true, data: null } });
 
// Formulaire → useForm
const form = useForm({ schema });
 
// Toggle simple → useState
const [isOpen, setIsOpen] = useState(false);

2. Organiser l'état global

snippet.javascript
// 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: []
    }
}

Points clés à retenir

  1. useGlobalStates pour les données partagées et persistées
  2. useStates pour l'état local avec path notation
  3. useForm pour les formulaires avec validation Zod
  4. Path notation : user.address.city, items[0], items[]
  5. Configurer storage.local pour la persistance

← Chapitre précédent | Retour au module | Chapitre suivant : Utilitaires →