react-singleton-state

1.0.4 • Public • Published

react-singleton-state 1.0.4

Author: Oleg Mukhov

Description:

Библиотека помогает быстро внедрять данные в React-компоненты путем записи и чтения объектов-синглтонов. Библиотека для тех, кто хочет быстро "пересесть" с Angular 1.x на React

Launch v1.0.0:

Так как при установке в node_modules приходит исходный каталог, необходимо добавить в webpack.config Вашего проекта следующую настройку js-модуля:

{
    test: /\.js$/,
    loader: 'babel-loader',
    query: {
        presets: ['es2017', 'react'],
        plugins: ['transform-object-rest-spread', 'transform-class-properties']
    }
}

дополнительно установите: npm install --save-dev babel-preset-es2017 babel-plugin-transform-class-properties babel-plugin-transform-object-rest-spread Проблема решена с версии 1.0.1.

Launch Sample App (since v1.0.2)

start index.html in browser node_modules/react-singleton-state/public/index.html You also need material-ui package to run the sample app.

There you can compare react-singleton-state and redux and choose which one is easier and more flexible for you.

Использование:

Библиотека состоит из следующих исполняемых классов и функций:

  1. Класс Provider - синглтон, основное хранилище приложения.
import { Provider, providerExporter } from 'react-singleton-state';
  1. Класс Service - от данного класса наследуются все сервисы приложения. После агрегации в Provider, они также становятся синглтонами.
import { Service } from 'react-singleton-state';
  1. Класс Component - обертка библиотеки для React-компонентов, связывает state-компонентов с инстансами Service.
import { Component } from 'react-singleton-state';

Provider

Класс Provider является синглтоном.

Использование Provider в приложении:

  • наследуем класс, например, AppProvider от Provider;
class AppProvider extends Provider { }
  • описываем метод defineServices() класса AppProvider, где просто агрегируем сервисы нашего приложения;
defineServices() {
    this.UserService = new UserService('UserService');
    this.TaskService = new TaskService('TaskService');
}
  • в случае необходимости можно описать необязательный метод defineUrls(), который сохранит все url'ы в константы по каскадному принципу;
defineUrls() {
    return {
        ROOT: {
            url: 'http://localhost:8090',
            REST: {
                url: '/rest',
                TASKS: {
                    url: '/tasks'
                },
                FOLLOWERS: {
                    url: '/followers'
                }
            },
            CREDENTIALS: {
                url: '/security/v1'
            }
        }
    };
}
/*  
 *  this.URLS = {
 *    ROOT:        'http://localhost:8090',
 *    REST:        'http://localhost:8090/rest',
 *    TASKS:       'http://localhost:8090/rest/tasks',
 *    FOLLOWERS:   'http://localhost:8090/rest/followers',
 *    CREDENTIALS: 'http://localhost:8090/security/v1'
 *  }
 */
  • Последним шагом экспортируем AppProvider в проект используя библиотечную функцию providerExporter(). Во вермя импорта произойдет вызов функции, которая вернет new AppProvider(), в результате чего результат такого импорта можно сразу представить в виде объекта URLS и необходимых сервисов.
export default providerExporter(AppProvider);
// ==========================================
import AppProvider from 'src/AppProvider';
const { URLS, UserService, TaskService } = AppProvider;

Методы, которые должны или могут быть описаны у наследника класса Provider:

  • Обязательный метод defineServices(). Не ожидает никаких аргументов. В теле метода необходимо присвоить полям класса, которые в последствии станут сервисами-синглтонами приложения, экземпляры классов-наследников класса Service. Метод вызывается в конструкторе класса Provider.
  • Необязательный метод defineUrls(). Не ожидает никаких аргументов. В теле метода необходимо вернуть объект, представляющий собой каскадируемые урлы. Поля url являются конечными полями в итерациях.

Принцип работы класса Provider

Provider является самовызывающейся функцией, которая возвращает класс. В конструкторе этого класса, сперва, происходит проверка на существование экземпляра этого класса, если такой имеется, то конструктор всегда вернет этот экземпляр. Такая проверка возможна, так как экземпляр класса и сам класс всегда находятся в одном замыкании. Если же экземпляра класса не обнаруживается, то последовательно вызываются методы defineServices() и defineUrls(). Затем происходит присвоение экземпляра класса в переменную внутри замыкания.

Service

Service - это бин. В сервисе есть только его поля, а также геттеры и сеттеры для обращения к ним.

Использование Service в приложении

  1. Наследуем новый бин от Service, определяем его поля, геттеры и сеттеры. (Начиная с версии 1.0.3, приватные поля определяются автоматически из defaultValues. Доступ к ним осуществляется через Symbol.for()).
export default class UserService extends Service {
    static defaultValues = {
        userName: 'DefaultName'
    };
    
    get userName() { return this[Symbol.for('userName')]; }
    set userName(val) { this[Symbol.for('userName')] = val; } 
}

2. Определяем значение полей по умолчанию через статическую переменную defaultValues, и сохраняем ссылку на класс используя метод getClass(). Ключи defaultValues должны совпадать с названиями геттеров и сеттеров. Метод getClass() перестал поддерживаться с версии 1.0.1 и будет полностью отменен с версии 1.1.0

/*
 *  Шаг не имеет смысла после обновлений 1.0.2 и 1.0.3
 */
export default class UserService extends Service {
    static defaultValues = {
        userName: 'admin'
    };
    constructor(sn) {
        super(sn);
        this.getClass(UserService);
        //other variables
    }
    //getters and setters
}
  1. Агрегируем UserService в AppProvider'e.
class AppProvider extends Provider {
    defineServices() {
        this.UserService = new UserService('UserService');
    }
}
  1. Дальнейшее использование Service тесно связано с использованием класса ComponentService

Методы класса Service:

  • getClass(classType: class) [DEPRECATED] - метод принимает в качестве аргумента класс и записывает его в поле this.classType. В каждом наследнике класса Service необходимо вызывать этот метод в конструкторе, передавая в него ссылку на себя. Данная процедура необходима для доступа к статической переменной, описанного в классе Service.
  • toDefault(prop: string) - метод переводит указанное поле (по имени сеттера) к значению данного поля в статической переменной defaultValues.
  • defaultAll() - переводит все сеттеры к их значениям в defaultValues.

Принцип работы класса Service

Класс Service содержит только одно приватное поле serviceName. Поле заполняется при создании экземпляра класса. Поле необходимо для заполнения this.state React-компонента. Далее поле доступно только через геттер. Автор настоятельно рекомендует передавать в конструктор Service'ов такое же строковое значение как и название класса этого сервиса! Поскольку экземпляры всех сервисов в приложении агрегированы в синглтон-Provider, то все они сами выступают синглтонами.

Component

Класс Component связывает наши React-компоненты с экземплярами Service'ов.

Использование Component в приложении:

  1. Наследуем новый statefull-компонент от Component'a и инжектим Service'ы в его this.state через метод this._injectServices(). Метод не помешает использовать компонентный (локальный) state.
export default class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            localStateField1: 'default',
            localStateField2: undefined
        };
        this._injectServices([RouteService, UserService], InjectMerging.AFTER);
    }
}
  1. Для чтения данных из Service'ов нам абсолютно не нужен this.state, поэтому данные также легко вставлять и в stateless-компоненты.
const Stateless = props => <p>{props.paragraph}</p>;
 
//render of some class
render() {
    return (
        <div>
            <Stateless paragraph={TextService.paragraph} />
        </div>
    );
}
 
  1. Для записи данных в Service есть два способа:
Первый способ необходим для изменения данных в Service без ререндеринга компонента. Для этого нужно использовать сеттер самого Service'a
export default class TaskItem extends Component {
    constructor(props) {
        super(props);
        this.state = {
            TaskService           
        };
    }
    onTaskChange(e) {
        TaskService.taskText = e.target.value;
    }
    render() {
        return (
            <div>
                <p>{TaskService.taskText}</p>
                <input value={TaskService.taskText} onChange={this.onTaskChange} />
            </div>
        );
    }
} 

В данном примере значение внутри Service'a будет изменяться, однако ни в input, ни в p, новое значение появляться не будет. Данный код можно оптимизировать, отображая значение в input, но не изменяя его в p. Для этого добавим поле компонентного state и метод lifecycle - componentWillUpdate() и componentWillUnmount. И поменяем данные в input.props.value.

export default class TaskItem extends Component {
    constructor(props) {
        super(props);
        this.state = {
            TaskService,
            taskText: TaskService.taskText           
        };
    }
    componentWillUpdate(nextProps, nextState) {
        this.state.taskText = TaskService.taskText;
    }
    componentWillUnmount() {
        TaskService.taskText = this.state.taskText;
    }
    onTaskChange(e) {
        this.setState({taskText: e.target.value});
    }
    render() {
        return (
            <div>
                <p>{TaskService.taskText}</p>
                <input value={this.state.taskText} onChange={this.onTaskChange.bind(this)} />
            </div>
        );
    }
} 

Из примера видно, каким мощным эффектом оптимизации без использования shouldComponentUpdate() обладают сервисы-синглтоны.

Второй способ изменения данных в сервисе влечет за собой ререндеринг компонента. Для его осуществления необходимо вызвать метод reRender(), который унаследован от Component
export default class TaskItem extends Component {
    constructor(props) {
        super(props);
        this.state = {
            TaskService           
        };
    }
    onTaskChange(e) {
        this.reRender(TaskService)('taskText').set(e.target.value);
    }
    render() {
        return (
            <div>
                <p>{TaskService.taskText}</p>
                <input value={TaskService.taskText} onChange={this.onTaskChange.bind(this)} />
            </div>
        );
    }
} 

Теперь при изменении данных внутри input будет происходить ререндеринг компонента, в результате чего в p и input будут отображаться актуальные значения.

Методы класса Component

  • _injectService() внедряет Service'ы в state компонента. Принимает два аргумента:
    1. Массив сервисов, которые будут внедрены в state компонента;
    2. enum InjectMerging с одним из значений: InjectMerging.BEFORE - внедрит сервисы перед значениями локального state, _InjectMerging.AFTER - после. Данный аргумент необязательный, по умолчанию используется InjectMerging.BEFORE
//InjectMerging.BEFORE
this.state = {
    //your services here
    first: 'first',
    second: 'second'
};
 
///InjectMerging.AFTER
this.state = {
    first: 'first',
    second: 'second',
    //your services here
};

Кроме того, можно обойтись и без метода _injectServices() при внедрении сервисов в state

constructor(props) {
    super(props);
    this.state = {
        UserService,
        first: 'first',
        second: 'second',
        RouteService
    };
}
  • _bindMethods() - метод принимает строковые наименования методов компонента, к которым применится .bind(this). Кроме того, каждый аргумент может быть массивом: ['имя_метода','аргумент1','аргумент2'].
constructor(props) {
    super(props);
    this._bindMethods('onTextChange', 'onSelectChange', 'onButtonClick');
    //======or=======
    this._bindMethods(
        ['onTextChange', 'newValue', SomeFilter.filter('Val')], 
        ['onSelectChange', 1]
    );
}
  • reRender() - метод необходим для изменения значения Service'a с последующим ререндеринго компонента. Метод устроен довольно непросто, так что разберем его подробно.
  1. reRender принимает один аргумент - экземпляр сервиса либо его поле this.serviceName и возвращает функцию...
this.reRender(UserService)   // return serviceProps => { ... }
this.reRender('UserService') // or this.reRender(UserService.serviceName) - return serviceProps => { ... }
  1. возвращаемая функция принимает один необязаетльный аргумент - массив строковых названий полей сервиса или строковое название одного поля сервиса, и возвращает объект методов.
this.reRender(UserService)('login') //return { set: val => {...}, setDefault: () => {...}  }
this.reRender(UserService)(['login', 'followers', 'dateOfBirth']) //return { set: values => {...}, setDefault: () => {...} }
this.reRender(UserService)() // return { set: obj => {...}, setDefault: () => {...} }
  1. Метод set() вызывает сеттер Service'a и делает forceUpdate: * Если возвращен по одному переданному имени поля, то принимает одно значение - новое значение этого поля в Servic'e; * Если возвращен по массиву имен полей, то принимает массив новых значений этих полей по соответствию индексов; * Если возвращен по пустому значению, то принимает объект c ключами - именами полей Service'a, и значениями - новыми значениями этих полей.
this.reRender(UserService)('login').set('MyName');
this.reRender(UserService)(['login', 'dateOfBirth']).set(['MyName', '22.06.1941']);
this.reRender(UserService)().set({login: 'MyName', dateOfBirth: '22.06.1941'});
  1. Метод setDefault() вызывает Service.toDefault() в первых двух случаях и Service.defaultAll() в третьем, после чего вызывает метод forceUpdate.

Package Sidebar

Install

npm i react-singleton-state

Weekly Downloads

1

Version

1.0.4

License

ISC

Last publish

Collaborators

  • olegdzhan