paint-brush
Як реалізувати шаблон дизайну стану в JavaScript та інтегрувати його з хуками Reactза@anonymouswriter_randomhandleasdad123125156135
457 показання
457 показання

Як реалізувати шаблон дизайну стану в JavaScript та інтегрувати його з хуками React

за anonymouswriter13m2025/03/22
Read on Terminal Reader

Надто довго; Читати

Я пишу цю статтю, тому що не знайшов рішення, схожого на моє, тому моє рішення може бути корисним для когось іншого. Ми впроваджуємо шаблон проектування стану саме так, як рекомендує гуру рефакторинга. Використовуйте шаблон стану в хуку react.
featured image - Як реалізувати шаблон дизайну стану в JavaScript та інтегрувати його з хуками React
anonymouswriter HackerNoon profile picture

Я пишу цю статтю, тому що не знайшов рішення, схожого на моє, тому моє рішення може бути корисним для когось іншого.

Зміст

  • Реалізація

    • Реалізувати заняття

    • Використовуйте шаблон стану в хуку react


  • Повний код, щоб ви могли копіювати та вставляти.


  • Розширений кінцевий автомат (стан помилки, HTML з можливістю копіювання)

    • Діаграма

    • Код


  • Які проблеми він вирішує?

  • Чому ця стаття має сенс.

Реалізація

Ми реалізуємо шаблон проектування стану так, як рекомендує гуру рефакторинга: https://refactoring.guru/design-patterns/state

Реалізація класів

 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})`; } }

Це поки що наша державна машина

діаграма кінцевого автомата


Використовуйте шаблон стану в хуку React

Наступна проблема: як ми використовуємо класи в поєднанні з react?

Інші статті використовують useEffect і рядок для збереження назви поточного стану; ми хочемо, щоб наша реалізація була чистою.

roomClient може змінювати стан, якщо він має посилання на функцію setState .


Проблеми:

  • Ми не можемо передати setState , якщо ініціалізуємо стан за допомогою класу.
  • Ми не хочемо повертати null із гачка.
  • Ми не хочемо повертати фіктивні методи, які нічого не повертають із гачка.


Рішення: надайте roomClient , як тільки стан ініціалізовано, прямо під 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; }

Повний код, щоб ви могли копіювати та вставляти

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

Розширений кінцевий автомат (стан помилки, HTML з можливістю копіювання)

Ми розширюємо кінцевий автомат, тому що ми хочемо перейти в стан Error , якщо ми намагаємося вийти з кімнати, і це призводить до помилкової операції. Це дозволяє нам відображати повідомлення про статус, викликаючи getStatusMessage .

Діаграма

оновлена діаграма кінцевого автомата зі станом помилки



Код

 <!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/react@18.3.1/umd/react.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/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>

Які проблеми він вирішує?

  • Ми можемо масштабувати кінцевий автомат, не змінюючи існуючий код.
  • Менше помилок.
  • Більш зрозумілий код, коли ми зрозуміємо, як він працює (все, що нам потрібно зробити, це додати новий клас для нового стану) .
  • Уникайте складних блоків if-else, складних мутацій стану та одного оператора switch.
  • Це добре, якщо ви хочете створювати кімнати в реальному часі за допомогою WebSockets (ми можемо відстежувати стан підключення до кімнати користувача та інші типи станів).

Чому ця стаття має сенс

Коли я шукав state design pattern в Google, це були мої перші результати

Посилання на 3 результати:


Пошук react state design pattern дає реалізації, які зовсім не схожі на реалізацію на https://refactoring.guru/design-patterns/state

Посилання на результати пошуку: