Я не начинал писать Looking Glass Engine как самостоятельный проект; скорее меня завербовали в Сопровождение Фректал, в которой было больше всего того, что мне нравилось в госуправлении. Однако, когда Looking Glass разрабатывалась, она превзошла Freactal, и в хорошем смысле.

Вот проблемы, которые я видел в состоянии, основываясь на своем опыте работы с React, Saga и Freactal:

  1. React ОЧЕНЬ многословен и препятствует быстрой разработке приложений.
  2. Создание свойств и методов их обновления было утомительным. Не существует однострочного метода для определения значения свойства по умолчанию, имени и метода обновления, а также нет проверки типа свойств.
  3. Асинхронные действия требуют обширного ритуала в saga и thunk, рассеивающего поток кода, который должен быть базовым и понятным локально.
  4. Системы текущего состояния предполагают, что состояние представляет собой глобальную нисходящую механику.
  5. Системы в текущем состоянии очень сложно тестировать как единый элемент, потому что они разработаны с учетом симбиотических отношений с элементами React, а не как автономные системы.
  6. Локальное состояние (которое я считаю изначально плохим) — это механика толчка, затрудняющая написание подпрограмм последовательного изменения.

Итак, когда я проектировал 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;