467 lectures
467 lectures

Comment implémenter le modèle de conception d'état en JavaScript et l'intégrer aux hooks React

par anonymouswriter13m2025/03/22
Read on Terminal Reader

Trop long; Pour lire

J'écris cet article car je n'ai pas trouvé de solution similaire à la mienne. Ma solution pourrait donc être utile à quelqu'un d'autre. Nous implémentons le modèle de conception d'état comme le recommande Refactoring Guru. Utilisez le modèle d'état dans le hook React.
featured image - Comment implémenter le modèle de conception d'état en JavaScript et l'intégrer aux hooks React
anonymouswriter HackerNoon profile picture

J'écris cet article car je n'ai pas trouvé de solution qui ressemble à la mienne, donc ma solution pourrait être utile à quelqu'un d'autre.

Table des matières

  • Mise en œuvre

    • Implémenter les classes

    • Utiliser le modèle d'état dans le hook React


  • Le code complet, vous pouvez donc le copier-coller.


  • Machine à états étendue (état d'erreur, HTML copiable-collable)

    • Diagramme

    • Code


  • Quels problèmes résout-il ?

  • Pourquoi cet article a du sens.

Mise en œuvre

Nous mettons en œuvre le modèle de conception d'état comme le recommande le gourou du refactoring : https://refactoring.guru/design-patterns/state

Implémenter les classes

 class RoomState { #roomClient = null; #roomId = null; constructor(roomClient, roomId) { if (roomClient) { this.#roomClient = roomClient; } if (roomId) { this.roomId = roomId; } } set roomClient(roomClient) { if (roomClient) { this.#roomClient = roomClient; } } get roomClient() { return this.#roomClient; } set roomId(roomId) { if (roomId) { this.#roomId = roomId; } } get roomId() { return this.#roomId; } join(roomId) { throw new Error('Abstract method join(roomId).'); } leave() { throw new Error('Abstract method leave().'); } getStatusMessage() { throw new Error('Abstract method getStatusMessage().'); } } // ------------------------------------------------------------------------- class PingRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PongRoomState(this.roomClient, roomId)); } leave() { const message = `Left Ping room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Ping room ${this.roomId}`; } } // ------------------------------------------------------------------------- class PongRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { const message = `Left Pong room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Pong room ${this.roomId}`; } } // ------------------------------------------------------------------------- class LeftRoomState extends RoomState { #previousRoom = null; constructor(roomClient, previousRoom) { super(roomClient); this.#previousRoom = previousRoom; } join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { throw new Error(`Can't leave, no room assigned`); } getStatusMessage() { return `Not in any room (previously in ${this.#previousRoom})`; } }

C'est notre machine d'état jusqu'à présent

diagramme de machine d'état


Utiliser le modèle d'état dans le hook React

Le problème suivant : comment utiliser les classes en combinaison avec React ?

Les autres articles utilisent useEffect et une chaîne pour stocker le nom de l'état actuel ; nous voulons garder notre implémentation propre.

Le roomClient peut modifier l'état, s'il a une référence à la fonction setState .


Problèmes:

  • Nous ne pouvons pas transmettre le setState si nous initialisons l'état avec la classe.
  • Nous ne voulons pas renvoyer null depuis le hook.
  • Nous ne voulons pas renvoyer de méthodes fictives qui ne renvoient rien du hook.


Solution, fournir le roomClient dès que l'état est initialisé, juste en dessous du useState .

 function useRoomClient() { const [state, setState] = useState(new PingRoomState()); // State contains the class // Initialize once // We can do this thanks to the `set` and `get` methods on // `roomClient` property if (!state.roomClient) { state.roomClient = { setState }; } return state; }

Le code complet pour que vous puissiez copier-coller

 class RoomState { #roomClient = null; #roomId = null; constructor(roomClient, roomId) { if (roomClient) { this.#roomClient = roomClient; } if (roomId) { this.roomId = roomId; } } set roomClient(roomClient) { if (roomClient) { this.#roomClient = roomClient; } } get roomClient() { return this.#roomClient; } set roomId(roomId) { if (roomId) { this.#roomId = roomId; } } get roomId() { return this.#roomId; } join(roomId) { throw new Error('Abstract method join(roomId).'); } leave() { throw new Error('Abstract method leave().'); } getStatusMessage() { throw new Error('Abstract method getStatusMessage().'); } } // ------------------------------------------------------------------------- class PingRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PongRoomState(this.roomClient, roomId)); } leave() { const message = `Left Ping room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Ping room ${this.roomId}`; } } // ------------------------------------------------------------------------- class PongRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { const message = `Left Pong room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Pong room ${this.roomId}`; } } // ------------------------------------------------------------------------- class LeftRoomState extends RoomState { #previousRoom = null; constructor(roomClient, previousRoom) { super(roomClient); this.#previousRoom = previousRoom; } join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { throw new Error(`Can't leave, no room assigned`); } getStatusMessage() { return `Not in any room (previously in ${this.#previousRoom})`; } } function useRoomClient() { const [state, setState] = useState(new PingRoomState()); // State contains the class // Initialize once // We can do this thanks to the `set` and `get` methods on // `roomClient` property if (!state.roomClient) { state.roomClient = { setState }; } return state; }

Machine à états étendue (état d'erreur, HTML copiable-collable)

Nous étendons la machine d'état afin de passer à l'état Error si nous tentons de quitter la pièce et que cela entraîne une erreur. Cela nous permet d'afficher des messages d'état en appelant getStatusMessage .

Diagramme

diagramme de machine d'état mis à jour avec état d'erreur



Code

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="root"></div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script> <script> class RoomState { #roomClient = null; #roomId = null; constructor(roomClient, roomId) { if (roomClient) { this.#roomClient = roomClient; } if (roomId) { this.roomId = roomId; } } set roomClient(roomClient) { if (roomClient) { this.#roomClient = roomClient; } } get roomClient() { return this.#roomClient; } set roomId(roomId) { if (roomId) { this.#roomId = roomId; } } get roomId() { return this.#roomId; } join(roomId) { throw new Error('Abstract method join(roomId).'); } leave() { throw new Error('Abstract method leave().'); } getStatusMessage() { throw new Error('Abstract method getStatusMessage().'); } } // ------------------------------------------------------------------------- class PingRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PongRoomState(this.roomClient, roomId)); } leave() { const message = `Left Ping room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Ping room ${this.roomId}`; } } // ------------------------------------------------------------------------- class PongRoomState extends RoomState { join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { const message = `Left Pong room ${this.roomId}`; this.roomClient.setState(new LeftRoomState(this.roomClient, message)); } getStatusMessage() { return `In the Pong room ${this.roomId}`; } } // ------------------------------------------------------------------------- class LeftRoomState extends RoomState { #previousRoom = null; constructor(roomClient, previousRoom) { super(roomClient); this.#previousRoom = previousRoom; } join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { // Extend to shift to error state this.roomClient.setState( new ErrorRoomState( this.roomClient, new Error(`Can't leave, no room assigned`), ), ); } getStatusMessage() { return `Not in any room (previously in ${this.#previousRoom})`; } } // Extend our state machine to hold one more state. class ErrorRoomState extends RoomState { #error = null; constructor(roomClient, error) { super(roomClient); this.#error = error; } join(roomId) { this.roomClient.setState(new PingRoomState(this.roomClient, roomId)); } leave() { // Do nothing... We can't move anywhere. We handled error. } getStatusMessage() { return `An error occurred. ${this.#error.message}`; } } const { useState } = React; function useRoomClient() { const [state, setState] = useState(new PingRoomState()); // State contains the class // Initialize once // We can do this thanks to the `set` and `get` methods on // `roomClient` property if (!state.roomClient) { state.roomClient = { setState }; } return state; } // ---------------------------------------------------------------------- // Usage example // ---------------------------------------------------------------------- const e = React.createElement; function useWithError(obj) {} function App() { const roomClient = useRoomClient(); return e( 'div', null, e('h1', null, 'Change room state'), e('p', null, `Status message: ${roomClient.getStatusMessage()}`), e( 'div', null, e('button', { onClick: () => roomClient.join('a') }, 'Join'), e('button', { onClick: () => roomClient.leave() }, 'Leave'), ), ); } const { createRoot } = ReactDOM; const root = document.getElementById('root'); createRoot(root).render(React.createElement(App)); </script> </body> </html>

Quels problèmes cela résout-il ?

  • Nous pouvons faire évoluer la machine d’état sans modifier le code existant.
  • Moins de bugs.
  • Code plus compréhensible, une fois que nous avons compris comment cela fonctionne (tout ce que nous avons à faire est d'ajouter une nouvelle classe pour un nouvel état) .
  • Évitez les blocs if-else compliqués, les mutations d’état complexes et une seule instruction switch.
  • C'est pratique si vous souhaitez créer des salles en temps réel à l'aide de WebSockets (nous pouvons surveiller l'état de connexion de la salle utilisateur et d'autres types d'états).

Pourquoi cet article est pertinent

Lorsque j'ai recherché state design pattern sur Google, voici mes premiers résultats

Liens vers les 3 résultats :


La recherche react state design pattern donne des implémentations qui ne ressemblent en rien à l'implémentation sur https://refactoring.guru/design-patterns/state

Liens vers les résultats de la recherche :


Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks