Я пишу цю статтю, тому що не знайшов рішення, схожого на моє, тому моє рішення може бути корисним для когось іншого.
Зміст
Реалізація
Реалізувати заняття
Використовуйте шаблон стану в хуку 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/[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>
Які проблеми він вирішує?
- Ми можемо масштабувати кінцевий автомат, не змінюючи існуючий код.
- Менше помилок.
- Більш зрозумілий код, коли ми зрозуміємо, як він працює (все, що нам потрібно зробити, це додати новий клас для нового стану) .
- Уникайте складних блоків if-else, складних мутацій стану та одного оператора switch.
- Це добре, якщо ви хочете створювати кімнати в реальному часі за допомогою WebSockets (ми можемо відстежувати стан підключення до кімнати користувача та інші типи станів).
Чому ця стаття має сенс
Коли я шукав state design pattern
в Google, це були мої перші результати
Посилання на 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