Я не начинал писать Looking Glass Engine как самостоятельный проект; скорее меня завербовали в Сопровождение Фректал, в которой было больше всего того, что мне нравилось в госуправлении. Однако, когда Looking Glass разрабатывалась, она превзошла Freactal, и в хорошем смысле.
Вот проблемы, которые я видел в состоянии, основываясь на своем опыте работы с React, Saga и Freactal:
- React ОЧЕНЬ многословен и препятствует быстрой разработке приложений.
- Создание свойств и методов их обновления было утомительным. Не существует однострочного метода для определения значения свойства по умолчанию, имени и метода обновления, а также нет проверки типа свойств.
- Асинхронные действия требуют обширного ритуала в saga и thunk, рассеивающего поток кода, который должен быть базовым и понятным локально.
- Системы текущего состояния предполагают, что состояние представляет собой глобальную нисходящую механику.
- Системы в текущем состоянии очень сложно тестировать как единый элемент, потому что они разработаны с учетом симбиотических отношений с элементами React, а не как автономные системы.
- Локальное состояние (которое я считаю изначально плохим) — это механика толчка, затрудняющая написание подпрограмм последовательного изменения.
Итак, когда я проектировал Looking Glass Engine, у меня было несколько требований, чтобы обойти это:
- Уметь быстро определять имя свойства, значение по умолчанию и (необязательно) тип, а также неявно создавать метод «set».
- Уметь писать асинхронные действия, не полагаясь на сложные артефакты.
- Уметь использовать систему как в качестве глобально доступного хранилища, такого как коллекции Redux или Backbone, так и для тактического управления сложным состоянием в компоненте.
- Иметь возможность тестировать взаимодействие состояний и действий, не требуя компонентов DOM или представления.
В результате Looking Glass Engine определяет объекты хранилища, которые являются независимыми экземплярами класса, на которые можно подписаться BehaviorSubjects. Они присоединяются к компонентам React с помощью подписки RxJS. *
Вот пример глобального хранилища для управления пользователями
import {Store} from '@wonderlandlabs/looking-glass-engine'; import tryToLogUserIn from './user/tryToLogIn'; const userStore = new Store({ actions: { loadUserFromLocalStorage(store) { /** ... omitted for brevity (**) */ }, saveUserToLocalStorage(store) { /** ... omitted for brevity ... (**) */ }, clearUserFromLocalStorage(store) { /** ... omitted for brevity ... (**) */ }, logout(store) { store.actions.clearUserFromLocalStorage(); store.actions.setUser(null); }, async login(store, username, password) { store.actions.setLoginError(false); try { let user = await tryToLogIn(username, password); store.actions.setUser(user); store.actions.saveUserToLocalStorage(user); } catch (err) { this.setLoginError(err); } } } }) .addProp('loginError') .addProp('user', storedUser || null, 'object'); userStore.actions.loadUserFromLocalStorage(); export default userStore;
Чтобы использовать это хранилище в компоненте входа:
import userStore from './userStore'; import React, {Component} from 'react'; export default class LoginForm extends Component { constructor(props) { super(props); this.state = {...userStore.state, username: '', password: ''}; this.doLogin = this.doLogin.bind(this); } componentDidMount() { this.sub = userStore.subscribe(({state}) => { this.setState(state); }); } componentWillUnmount() { if (this.sub) { this.sub.unsubscribe(); } } doLogin() { const {username, password} = this.state; userStore.actions.login(username, password); } render() { const {username, password, loginError} = this.state; return ( <div className="login-form"> {loginError ? <p>{loginError.message}</p> : ''} <div className="form-row"> <label>Username</label> <input type="text" value={username} onChange={({target}) => { this.setState({username: target.value}) }}/> </div> <div className="form-row"> <label>Password</label> <input type="password" value={password} onChange={({target}) => { this.setState({password: target.value}) }}/> </div> <div className="form-row"> <button onClick={this.doLogin}>Log In</button> </div> </div> ) } }
Или в пользовательской панели.
export default class UserPanel extends Component { constructor(props) { super(props); this.state = {user: userStore.state.user}; this.doLogin = this.doLogin.bind(this); } componentDidMount() { this.sub = userStore.subscribe(({state}) => { this.setState({user: state.user}); }); } componentWillUnmount() { if (this.sub) { this.sub.unsubscribe(); } } render() { const {user} = this.state; return ( <section className="user-panel"> {user ? <span> {`welcome ${user.username}`} <button onClick={() => { userStore.actions.logout(); document.location = '/'; // or history.push('/') if react-router used }}>Log Out</button> </span> : <a href="/login">Log In</a> } </section> ) } }
Действия
Действия просты в использовании и мощны в использовании.
Действия могут принимать аргументы, как и в случае с описанным выше методом входа в систему. Ваши аргументы будут переданы действию после внутреннего свойства хранилища, которое всегда будет первым. (вам не нужно проходить магазин самостоятельно.)
- Действия неразрывно связаны с контекстом хранилища.
- Если действие возвращает значение, это значение теперь является состоянием (например, действие Redux).
- Если действие не имеет возвращаемого значения, то никаких изменений не предполагается.
- Первым параметром действия является сам объект хранилища, поэтому вы можете вызывать другие действия из этого хранилища, включая свойства установки для свойств (см. ниже).
- Если вы меняете состояние внутри действия, свойство store.state обновляется синхронно.
- После каждого действия (включая вызовы установщика свойств) само хранилище передается подписчикам с текущим состоянием в качестве подсвойства.
Эти правила означают, что вы можете писать простые синхронные действия, которые изменяют несколько свойств — либо одним махом, возвращая обновленную версию состояния (стиль Redux), либо одно за другим, вызывая действия установки свойств внутри вашего действия.
И если вы хотите/нужно использовать асинхронные действия и внешние асинхронные системы, вы можете смешивать их без каких-либо специальных инструментов.
Внутри вашего действия вы всегда можете рассчитывать на актуальность свойства состояния параметров (при условии, что вы не деструктурируете его).
И если ваш магазин является глобальным (или вы предоставляете его как свойство для подпредставления), вы можете вызывать действия напрямую — вам не нужно смешивать их с реквизитами или состоянием компонентов React.
Асинхронные процессы в действиях
Действия являются синхронными, когда они могут быть, и асинхронными, когда они должны быть.
- Если действие возвращает Promise, это обещание раскручивается, и правила действий продолжаются с него асинхронно.
- Если действие возвращает функцию, эта функция вызывается, и ее результат обрабатывается по правилам действий.
- Действия возвращают обещания. это не означает, что действие является асинхронным, обязательно — действия настройки свойств и действия, которые не являются асинхронными и не возвращают промисы, выполняются напрямую. На самом деле это означает, что в таких случаях вам гарантируется, что все асинхронные действия будут развернуты и завершены после завершения действия return Promise.
В результате, если вы объявите действие асинхронным, вы сможете кодировать, используя все обычные асинхронные инструменты внутри своего действия.
Определение свойства
«Свойство» — это поле в состоянии магазина. Это утилита определения, которая одним махом добавляет начальное значение в состояние и метод установки. Определение свойства — одно из самых важных средств LGE, позволяющих сэкономить время. Хотя вы можете определить состояние как вход в конструктор (const myStore = new Store({state: {a: 1, b:2}, action: {…}), на практике определение свойств по одному быстрее и более Все это допустимые способы определения свойств:
const ShoppingCart = new Store ({ actions: {}, props: { total: 0, coupons: { start: () => ([]), type: 'array' } } }) .addProp('cartState', 'shopping', 'string') .addProp('cartError', null) .addProp('cartId');
Эта корзина будет иметь синхронные действия setCartState(value), setCart
Если тип определен (либо как третье свойство addProp, либо как свойство типа объекта описания), то вызов set[propName]()
Другие преимущества использования LGE
Хранилища LGE чрезвычайно легко тестируются, не требуя каких-либо представлений или интеграции реакции в модульные тесты. Изменения данных можно наблюдать, подписавшись на магазин.
Магазины LGE можно объединять для разделения связанных функций.
Магазины LGE могут прослушивать друг друга — если ваш магазин является глобальным, вы можете создавать подписки на один магазин и при обнаружении определенных изменений вызывать действия в другом магазине.
Действия LGE Stores перечислены в свойстве, поэтому вам не нужно гадать, что это за действия (в отличие от Redux).
Магазины LGE не предназначены для какой-либо конкретной версии React; их можно использовать везде, где есть наблюдаемые объекты RxJS, включая Angular, серверную часть и т. д.
Действия в LGE Store можно регулировать с помощью транзакций, чтобы синхронизировать все обновления действия (в том числе в поддействиях) в одно изменение по завершении действия (или выполнить откат в случае ошибок).
Ошибки LGE Store попадают в обработчик ошибок подписки, поэтому все ошибки можно наблюдать из центрального места.
Действия LGE Stores можно добавлять после создания с помощью методов addAction() и addProp(), поэтому вы можете писать функции для внешнего изменения хранилищ.
Магазины LGE имеют необязательный поток отладчика, который предоставляет подробную информацию о каждом обновлении магазина, если вам нужно проанализировать деятельность вашего магазина.
BehaviorSubject LGE Stores отображается в свойстве .stream магазина, поэтому, если вы хотите воспользоваться преимуществами регулирования или постобработки RxJS, вы можете передать поток из .stream в любые фильтры или управляющие механизмы RxJS, которые вы хотите.
Я обнаружил, что LGE значительно повышает мою производительность и помогает мне избежать многословных джунглей управления состоянием семейства Redux и любопытных задержек состояния компонентов. Я надеюсь, что вы найдете его полезным при разработке собственных приложений.
(*) Ранее я разработал HOC для Looking Glass Engine; но, наблюдая за тем, как люди взаимодействуют с ними, я заметил, что, не участвуя в процессе привязки, они зацикливались на своем «правильном» способе присоединения состояния к компонентам, вместо того чтобы осознавать, что у них есть варианты, как объединить состояние и действия в компонент.
(**) полный код с сохранением в магазине:
const userStore = new Store({ actions: { loadUserFromLocalStorage(store) { let userString = localStorage.getItem('user'); try { store.actions.setUser(JSON.parse(userString)); } catch (err) { store.actions.clearUserFromLocalStorage(); } }, saveUserToLocalStorage(store) { /** ... omitted for brevity ... (**) */ const userObject = store.actions.user; if (!userObject) { localStorage.removeItem('user'); return; } try { localStorage.setItem('user', userString); } catch (err) { store.actions.clearUserFromLocalStorage(); } }, clearUserFromLocalStorage(store) { localStorage.removeItem('user'); store.actions.setUser(null); }, logout(store) { store.actions.clearUserFromLocalStorage(); }, async login(store, username, password) { store.actions.setLoginError(false); try { let user = await tryToLogIn(username, password); store.actions.setUser(user); store.actions.saveUserToLocalStorage(user); } catch (err) { this.setLoginError(err); } } } }) .addProp('loginError') .addProp('user', storedUser || null, 'object'); userStore.actions.loadUserFromLocalStorage(); export default userStore;