Я пишу цю статтю, тому що не знайшов рішення, схожого на моє, тому моє рішення може бути корисним для когось іншого. Зміст Реалізація Реалізувати заняття Використовуйте шаблон стану в хуку 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 (ми можемо відстежувати стан підключення до кімнати користувача та інші типи станів). Чому ця стаття має сенс Коли я шукав в Google, це були мої перші результати state design pattern Посилання на 3 результати: https://refactoring.guru/design-patterns/state https://en.wikipedia.org/wiki/State_pattern https://www.geeksforgeeks.org/state-design-pattern/ https://medium.com/@udaykale/evolving-the-state-design-pattern-e6682a866fdd Пошук дає реалізації, які зовсім не схожі на реалізацію на react state design pattern https://refactoring.guru/design-patterns/state Посилання на результати пошуку: https://medium.com/@yah.emam/state-design-pattern-using-react-hooks-c535e1daa6f1 https://blog.logrocket.com/modern-guide-react-state-patterns/ https://github.com/themithy/react-design-patterns/blob/master/doc/state-pattern.md https://refine.dev/blog/react-design-patterns/ https://react.dev/learn/choosing-the-state-structure