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 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 :

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 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

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

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';
 
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

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

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

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

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: []
    }
}

useConfirm

Hook pour afficher des dialogues de confirmation et d'alerte modaux.

Import

snippet.javascript
import { useConfirm } from '@cap-rel/smartcommon';

Fonctions retournées

Le hook retourne un objet avec deux fonctions :

snippet.javascript
const { confirm, alert } = useConfirm();

Utilisation de confirm

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

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

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

snippet.javascript
import { usePWAUpdate } from '@cap-rel/smartcommon';

Utilisation

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

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

snippet.javascript
import { UpdatePrompt } from '@cap-rel/smartcommon';

Variantes d'affichage

Trois variantes sont disponibles :

Utilisation directe

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

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

  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 manuelle
  4. useConfirm pour les dialogues de confirmation et d'alerte
  5. usePWAUpdate pour détecter les mises à jour PWA
  6. UpdatePrompt pour afficher une UI de mise à jour prête à l'emploi
  7. Path notation : user.address.city, items[0], items[]
  8. Configurer storage.local pour la persistance

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