├── README.md ├── api.js ├── docs └── ru.md ├── fsm ├── machine.js ├── states.js └── transitions.js ├── index.html ├── index.js ├── renderer.js └── utils.js /README.md: -------------------------------------------------------------------------------- 1 | # Finite State Machine on Frontend 2 | 3 | > Read this [in Russian](./docs/ru.md). 4 | 5 | How to use Finite State Machines to manage state and transform UI in frontend applications. 6 | 7 | - [Sample App](https://bespoyasov.me/showcase/fsm/) 8 | - [Post About It](https://bespoyasov.me/blog/fsm-to-the-rescue/) 9 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | // Fetches posts list from json placeholder 2 | const fetchPosts = async () => { 3 | const response = await fetch('https://jsonplaceholder.typicode.com/posts') 4 | const posts = await response.json() 5 | return posts.slice(0, 5) 6 | } -------------------------------------------------------------------------------- /docs/ru.md: -------------------------------------------------------------------------------- 1 | # Управление состоянием приложения с помощью конечных автоматов 2 | 3 | > Read this [in English](../README.md). 4 | 5 | Пост о том, как использовать конечные автоматы для управления состоянием фронтенд-приложений, и небольшое приложение-пример: 6 | 7 | - [Приложение](https://bespoyasov.ru/fsm/) 8 | - [Пост](https://bespoyasov.ru/blog/fsm-to-the-rescue/) 9 | -------------------------------------------------------------------------------- /fsm/machine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements FSM abstraction. 3 | * @param {String} initial initial state for machine 4 | * @param {Object} states all possible states 5 | * @param {Object} transitions all transition functions 6 | * @param {Any} data data to store 7 | */ 8 | class StateMachine { 9 | constructor({ 10 | initial, 11 | states, 12 | transitions, 13 | data=null, 14 | }) { 15 | this.transitions = transitions 16 | this.states = states 17 | this.state = initial 18 | this.data = data 19 | 20 | this._onUpdate = null 21 | } 22 | 23 | stateOf() { 24 | return this.state 25 | } 26 | 27 | _updateState(newState, data=null) { 28 | this.state = newState 29 | this.data = data 30 | 31 | this._onUpdate 32 | && this._onUpdate(newState, data) 33 | } 34 | 35 | async performTransition(transitionName) { 36 | const possibleTransitions = this.transitions[this.state] 37 | const transition = possibleTransitions[transitionName] 38 | if (!transition) return 39 | 40 | const current = { 41 | state: this.state, 42 | data: this.data, 43 | } 44 | 45 | for await (const {newState, data=null} of transition(current)) { 46 | this._updateState(newState, data) 47 | } 48 | } 49 | 50 | subscribe(event, callback) { 51 | if (event === 'update') this._onUpdate = callback || null 52 | } 53 | } -------------------------------------------------------------------------------- /fsm/states.js: -------------------------------------------------------------------------------- 1 | // All possible state for system 2 | const states = { 3 | INITIAL: 'idle', 4 | LOADING: 'loading', 5 | SUCCESS: 'success', 6 | FAILURE: 'failure', 7 | } -------------------------------------------------------------------------------- /fsm/transitions.js: -------------------------------------------------------------------------------- 1 | // To set loading state, perform a network request 2 | // and set success or failure state after request is done. 3 | // Clears all older posts 4 | async function* loadNewPosts() { 5 | yield message(states.LOADING) 6 | const performRequest = tryCatchWrapper(states.SUCCESS, states.FAILURE) 7 | yield* performRequest(fetchPosts) 8 | } 9 | 10 | // To append new posts after existing. 11 | // Example of usage FSM with data storage inside 12 | async function* appendPosts(current) { 13 | yield message(states.LOADING, current.data) 14 | const performRequest = tryCatchWrapper(states.SUCCESS, states.FAILURE) 15 | 16 | for await (const update of performRequest(fetchPosts)) { 17 | const {newState, data=null} = update 18 | const newData = data !== null 19 | ? [...current.data, ...data] 20 | : null 21 | 22 | yield message(newState, newData) 23 | } 24 | } 25 | 26 | // To clear data and return to initial state 27 | function* clear() { 28 | yield message(states.INITIAL) 29 | } 30 | 31 | 32 | /** 33 | * Dict of all possible transitions between states. 34 | * Each transition func returns a new state which 35 | * FSM should be in after transition 36 | */ 37 | const transitions = { 38 | [states.INITIAL]: { 39 | fetch: loadNewPosts, 40 | }, 41 | 42 | [states.LOADING]: {}, 43 | 44 | [states.SUCCESS]: { 45 | loadMore: appendPosts, 46 | reload: loadNewPosts, 47 | clear: clear, 48 | }, 49 | 50 | [states.FAILURE]: { 51 | retry: loadNewPosts, 52 | clear: clear, 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |