Analyse des fondements de l'état partagé dans Hox pour React

Hox est une bibliothèque légère de gestion d'état pour React. Plutôt que de s'appuyer sur un contexte global unique ou un store externalisé, elle exploite les Hooks React natifs pour partager l'état entre composants. Examinons son architecture interne.

L'architecture fondamentale : la classe Store

Le cœur de Hox réside dans une abstraction appelée Store, qui encapsule un Hook React et gère les notifications aux consommateurs.

export class Store<returnedvalue hookarguments=""> {
  constructor(public readonly useHook: (args: HookArguments) => ReturnedValue) {}
  private subscribers: Set<(value: ReturnedValue) => void> = new Set();
  currentValue!: ReturnedValue;

  public notifySubscribers() {
    this.subscribers.forEach(callback => callback(this.currentValue));
  }
}
</returnedvalue>

Une instance de Store contiant une référence vers le Hook d'origine (useHook). Elle maintient un ensemble de fonctions de rappel (subscribers) qui seront notifiées à chaque mise à jour de la valeur courante (currentValue). Cela implémente un patron observateur simple pour la réactivité.

Création des stores partagés

Hox propose deux fonctions pour transformer un Hook classique en store partagé.

Le cas local avec createStore

Cette fonction crée un store dont la portée est liée à un composant React. Son implémentation interne utilise les Hooks de base.

export function createStore<returnedvalue any="" extends="" hookarguments="">(
  useOriginalHook: (...args: HookArguments) => ReturnedValue,
) {
  // 1. Créer une instance stable du Store via useState
  const storeInstance = useState(() => new Store(useOriginalHook))[0];

  // 2. Le composant Provider qui exécute le Hook original
  const Provider: React.FC<{ args: HookArguments; children: React.ReactNode }> = ({
    args,
    children,
  }) => {
    const value = useOriginalHook(...args);
    // 3. Met à jour le store et notifie les abonnés si la valeur a changé
    useEffect(() => {
      if (storeInstance.currentValue !== value) {
        storeInstance.currentValue = value;
        storeInstance.notifySubscribers();
      }
    });

    return children;
  };

  // 4. Le Hook consommateur qui s'abonne aux changements
  const useSharedStore = () => {
    const [, forceRender] = useState({});
    useEffect(() => {
      const onStoreUpdate = () => forceRender({});
      storeInstance.subscribers.add(onStoreUpdate);
      return () => { storeInstance.subscribers.delete(onStoreUpdate); };
    }, [storeInstance]);

    return storeInstance.currentValue;
  };

  return [useSharedStore, Provider] as const;
}
</returnedvalue>

Le composant Provider exécute le Hook fourni. Dès que le Hook retourne une nouvelle valeur, celle-ci est propagée via le store. Le Hook useSharedStore force le re-rendu du composant consommateur lorsque l'état change.

Le cas global avec createGlobalStore

Pour un état devant être accessible partout dans l'application, Hox propose une variante globale. La différance majeure est l'initialisation unique et hors d'un arbre de composants React spécifique.

export function createGlobalStore<returnedvalue>(
  useGlobalHook: () => ReturnedValue,
) {
  // Initialisation unique, en dehors de tout composant React
  const globalStoreInstance = new Store(useGlobalHook);

  // Un composant racine qui exécutera le Hook une fois pour toutes
  const GlobalProvider: React.FC = ({ children }) => {
    const value = useGlobalHook();
    useEffect(() => {
      globalStoreInstance.currentValue = value;
      globalStoreInstance.notifySubscribers();
    });
    return children;
  };

  const useGlobalSharedStore = () => {
    const [, forceRender] = useState({});
    useEffect(() => {
      const subscriber = () => forceRender({});
      globalStoreInstance.subscribers.add(subscriber);
      return () => { globalStoreInstance.subscribers.delete(subscriber); };
    }, []);

    return globalStoreInstance.currentValue;
  };

  return [useGlobalSharedStore, GlobalProvider] as const;
}
</returnedvalue>

Ici, l'instance globalStoreInstance est créée une seule fois lors de l'importation du module. Le GlobalProvider n'a besoin d'être monté qu'une seule fois à la racine de l'application pour initialiser la valeur.

Gestion de l'abonnement et des mises à jour

La réactivité de Hox repose sur un mécanisme d'abonnement simplifié, géré par le cycle de vie des Hooks.

// Illustration du mécanisme d'abonnement dans un Hook consommateur
function useSubscribedStore(storeInstance: Store) {
  const [localValue, setLocalValue] = useState(storeInstance.currentValue);

  useEffect(() => {
    // Fonction de rappel pour synchroniser la valeur locale
    const syncValue = (newValue: any) => setLocalValue(newValue);
    // Abonnement lors du montage du composant
    storeInstance.subscribers.add(syncValue);
    // Désabonnement lors du démontage
    return () => { storeInstance.subscribers.delete(syncValue); };
  }, [storeInstance]);

  return localValue;
}

Ce schéma garantit que seuls les composants effectivement abonnés à un store seront re-rendus lors de son changement, évitant les re-rendus inutiles dans d'autres parties de l'arbre.

Bonnes pratiques d'utilisation

Pour exploiter Hox efficacement :

  • Choisissez createStore pour un état partagé entre quelques composants frères ou parents/enfants directs.
  • Utilisez createGlobalStore pour des données véritablement globales, comme un thème ou un état d'authentification.
  • Séparez les états qui changent fréquemment de ceux qui sont stables dans des stores distincts pour optimiser les performances.
  • Exploitez le typage fort de TypeScript avec les génériques pour garantir la sûreté des types à travers les frontières des composants.

L'approche de Hox, ancrée dans les mécanismes fondamentaux de React, offre une solution de partage d'état élégante qui minimise les abstractions supplémentaires.

Étiquettes: React Hooks State Management TypeScript Hox

Publié le 30 mai à 09h05