Хабрахабр

Вам не нужен Redux

Uninstall Redux

Очередная статья, которая, возможно, так и останется в черновиках, но если вы это читаете, то все-таки это свершилось.

Благо, архитектура приложения позволяла его выпилить безболезненно. К написанию статьи послужил опыт с Redux, потому что повестись на хайп было опрометчивым решением. =)

Почему-то эта опьяняющая очевидность ко мне пришла далеко спустя время, хотя я имею опыт в написании приложений под Android, где никакого Redux нет и все живы, и здоровы. Долгое время и огромное количество человек я расспрашивал про то, как они используют Redux и всегда удивлялся.

К примеру, в нашем проекте Redux оправдывает себя только в нескольких местах, остальное можно реализовать тупо на Dumb & Smart Components. Вся проблема в том, что никто не объясняет зачем нужен и когда нужен Redux, пока ты не наступил на эти грабли спустя время. Если в приложении мало сепаратных частей, которые друг на друге могли бы быть зависимы, то это маловыгодная вещь. И в тех самых местах, где он оправдан, я бы также его убрал и использовал события. Но люди, почему-то, ссут подумать своей головой. Отсюда и появляются всякие оптимизационные костыли (reselect, например). И сейчас я вам поведаю сказ о том, как жить без Redux.

Моделирование ситуации

Эта ситуация, когда мы имеем два сепаратных компонента, где изменение в одном компоненте должно затрагивать изменение второго. Я не буду далеко ходить и возьму самую простую ситуацию, которую можно легко понять и быстро реализовать. Дабы уменьшить код и не писать компонент списка товаров я предлагаю просто реализовать в одном месте отображение количества, а во втором кнопку инкремента. Например, счетчик количества товара в корзине (первый компонент CartInfo) и список товаров в таблице (второй компонент Cart). Схематично это должно выглядеть так:

Schema of a situation

мы видим, что компоненты между собой никак не связаны. Т.е. Redux предлагает "простое" решение — это, использовать Flux-архитектуру. Поэтому, как-то по воздуху или телепатическими способностями нужно поделиться с ними (между ними) информацией. И она вполне работает, пока приложение не разрослось.

Если у вас есть возможность не использовать Redux — не используйте. Я не рассматриваю здесь никакие примеры, вроде работы с формами или сложная бизнес-логика, чтобы, как говорят, "ощутить преимущества без Redux", просто потому, что без Redux это и так ощутимо, весь геморой начинается с ним. Мысли по этому поводу можно еще подчерпнуть здесь.

Реализация компонентов

слой вида, который будет использоваться в каждом из примеров. Давайте напишем stateless-компоненты, т.е.

Компонент Cart:

// src/components/cart.js import React, from "react"; class Cart extends Component { render() { return ( <button type="button" onClick={this.props.onIncQnt}>Increment</button> ); }
} export default Cart;

Компонент CartInfo:

// src/components/cart-info.js import React, { Component, Fragment } from "react"; class CartInfo extends Component { render() { return ( <Fragment>Quantity: {this.props.qnt}</Fragment> ); }
} export default CartInfo;

Впихиваем в точку входа:

// src/index.js import React, { PureComponent } from "react";
import ReactDOM from "react-dom";
import Cart from "./components/cart";
import CartInfo from "./components/cart-info"; class App extends PureComponent { render() { return ( <div className={"app"}> <CartInfo qnt={0} /> <Cart onIncQnt={() => {}} /> </div> ); }
} ReactDOM.render(<App />, document.getElementById("root"));

Далее я буду рассматривать три варианта реализации. На этом общий код закончился.

Вариант первый. Smart & Dumb Components

Демо и исходный код:

Вроде, что может быть проще? Самое простое и быстрое решение — разбить код на две составляющие: stateless-компоненты (dumb) и stateful-компоненты (smart). И хочу заметить, что это основополагающий подход, который будет с вами от и до независимо от того, что вы используете: Redux, MobX или что-то другое. Если нет никакой сложной бизнес-логики (а чаще всего оно так и есть), то решение идеальное. Более подробно можно подчерпнуть здесь. Это, пожалуй, самое первое, что нужно изучить.

Принцип работы:

Principe of smart and dumb components

=)). Эта не самая простая схема принципа работы, но единственная в интернете, которая отражает реальность вещей (самому рисовать мне лень. Если закрыть глаза на слово "component", то это MVVM (речь о котором пойдет в третьем варианте). Что мы на ней видим? Что может быть проще? Это, конечно, "грязный" MVVM, но тем не менее: Smart-компонент работает за ViewModel, Services за Model, а dumb-компонент работает за View. Если в приложении не будет сложной бизнес логики, то все приложение можно пилить по этому принципу, если будет, то в тех местах просто используем "не грязный" MVVM (см. В этом и суть. третий вариант).

Добавим CartPage контейнер (smart/stateful-компонент), который и будет связывать наши два компонента (dumb/stateless-компоненты): И так, нам нужно нажать на кнопку и чтобы во втором компоненте изменялось количество.

// src/containers/cart-page.js import React, { Component } from "react";
import Cart from "./../components/cart";
import CartInfo from "./../components/cart-info"; class CartPage extends Component { constructor(props) { super(props); this.state = { qnt: 0 }; } render() { return ( <div className={"cart-page"}> <CartInfo qnt={this.state.qnt} />{" "} <Cart onIncQnt={this._onIncQnt.bind(this)} /> </div> ); } _onIncQnt() { this.setState(state => ({ qnt: state.qnt + 1 })); }
} export default CartPage;

За счет того, что CartPage имеет приоритет выше, чем наши компоненты, мы смогли добиться взаимодействия между ними (компонентами). Что здесь произошло: мы определили обработчик события для нашей кнопки, который увеличивает счетчик и передали показания счетчика и сам обработчик в наши компоненты.

А наша точка входа App изменится следующим образом:

// src/index.js import React, { PureComponent } from "react";
import ReactDOM from "react-dom";
import CartPage from "./containers/cart-page"; class App extends PureComponent { render() { return ( <div className={"app"}> <CartPage /> </div> ); }
} ReactDOM.render(<App />, document.getElementById("root"));

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

Где они должны быть? Хорошо, — скажете вы, — а как насчет запросов к API? Опять же, все упирается в архитектуру: запросы к API — это, совсем другая ответственность, а следовательно, нам нужно их где-то разместить. Здесь все еще проще. Для этого существуют репозитории.

Добавим репозиторий cart:

// src/repositories/cart.js let fakeServerState = { qnt: 0
}; export default { getQnt, incQnt
}; export function getQnt() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(fakeServerState.qnt); }, 1000); });
} export function incQnt() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(++fakeServerState.qnt); }, 1000); });
}

Изменим CartPage:

// src/containers/cart-page.js import React, { Component } from "react";
import Cart from "./../components/cart";
import CartInfo from "./../components/cart-info";
import { incQnt, getQnt } from "./../repositories/cart"; class CartPage extends Component { constructor(props) { super(props); this.state = { qnt: 0, wait: false }; } componentDidMount() { getQnt().then(payload => { this.setState({ qnt: payload }); }); } render() { return ( <div className={"cart-page"}> <CartInfo qnt={this.state.qnt} />{" "} <Cart onIncQnt={this._onIncQnt.bind(this)} wait={this.state.wait} /> </div> ); } _onIncQnt() { this.setState({ wait: true }); incQnt().then(payload => { this.setState({ qnt: payload, wait: false }); }); }
} export default CartPage;

Прозрачно и понятно. Вот и все.

Вариант второй. Redux

Демо и исходный код:

Как это делается мы опустим, т.к. Для этой задачи нам понадобится установить и настроить Redux. Кто знаком с Redux, тот поймет, а кто нет — можете пропустить этот шаг, вы ничего не потеряете. не является сутью статьи. Если очень хочется разобраться, то ссылка.

Для того, чтобы Redux работал, нужно установить и настроить пакеты:

  • redux;
  • react-redux;

А если еще и оптимизацией заниматься в будущем, потому что иначе никак, то… ууу… целый квест:

No, thanks

Но, перейдем к сути и не будем говорить о жопочасах.

Принцип работы:

Redux principles

Добавим редьюсер cart:

// src/reducers/cart.js const INIT_STATE = { qnt: 0
}; export default function cart(state = INIT_STATE, action) { switch (action.type) { case "CART_INC_QNT": return { ...state, qnt: state.qnt + 1 }; default: return state; }
}

Добавим экшн incQnt():

// src/actions/cart.js export function incQnt() { return { type: "CART_INC_QNT" };
}

Сконфигурируем хранилище:

// src/stores/configure.js import { createStore } from "redux";
import rootReducer from "./../reducers/cart"; export default function configure(initialState) { return createStore(rootReducer, initialState);
}

Модифицируем точку входа:

// src/index.js import React, { PureComponent } from "react";
import ReactDOM from "react-dom";
import CartPage from "./containers/cart-page";
import { Provider } from "react-redux";
import configureStore from "./stores/configure"; class App extends PureComponent { render() { return ( <div className={"app"}> <CartPage /> </div> ); }
} ReactDOM.render( <Provider store={configureStore()}> <App /> </Provider>, document.getElementById("root")
);

Модифицируем наш контейнер:

// src/containers/cart-page.js import React, { Component } from "react";
import Cart from "./../components/cart";
import CartInfo from "./../components/cart-info";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "./../actions/cart"; class CartPage extends Component { render() { const { incQnt } = this.props.actions; return ( <div className={"cart-page"}> <CartInfo qnt={this.props.cart.qnt} /> <Cart onIncQnt={incQnt} /> </div> ); }
} function mapStateToProps(state) { return { cart: state };
} function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(actions, dispatch) };
} export default connect( mapStateToProps, mapDispatchToProps
)(CartPage);

Самое главное, что изменилось — это, появилось центральное хранилище, которое будет расти по мере роста самого проекта и заставлять ререндириться всем компонентам каждый раз после обновления хранилища. На этом все. В целом, такой подход имел бы право на жизнь, если приложение не содержит бизнес-логики, которую, по сути, логично размещать в экшнах (а именно обращаться к нужным сервисам из экшнов). Чтобы этого избежать придется повозиться. А ведь еще нужно и передать все эти данные в экшн! Но данное решение приводит к тому, что придется каждый раз, когда нам потребуется перерасчет (например, скидки за товар на основе цены, количества и действующих акций) вызывать экшен, который вызовет обновление хранилища, что вызовет перерисовку всех подписанных контейнеров, а за ними и компонентов. Представьте форму из 20+ полей и все эти поля нужно постоянно передавать по кругу. И так по кругу. Нужно ли это? И чем больше приложение, тем глубже и больше дерево редьюсеров, тем более геморойней поддержка и оптимизация, и т.п. Решать вам.

Ведь у нас счетчик обновляется моментально. И да, конечно, вы вспомнили о асинхронном запросе! Да, тогда нам потребуется еще один пакет: react-thunk. А что, если API?

Доработаем наш конфигуратор хранилища:

// src/stores/configure.js import { createStore, applyMiddleware } from "redux";
import rootReducer from "./../reducers/cart";
import thunk from "redux-thunk"; export default function configure(initialState) { return createStore( rootReducer, initialState, applyMiddleware(thunk) );
}

Доработаем экшн incQnt() и добавим новый — load():

// src/actions/cart.js let fakeServerState = { qnt: 0
}; export function incQnt() { return dispatch => { dispatch({ type: "CART_INC_QNT" }); return new Promise((resolve, reject) => { setTimeout(() => { resolve(++fakeServerState.qnt); }, 1000); }).then(qnt => { dispatch({ type: "CART_INC_QNT_SUCCESS", payload: qnt }); }); };
} export function load() { return dispatch => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(fakeServerState.qnt); }, 1000); }).then(qnt => { dispatch({ type: "CART_LOAD", payload: qnt }); }); };
}

Доработаем редьюсер cart:

// src/reducers/cart.js const INIT_STATE = { qnt: 0, wait: false
}; export default function cart(state = INIT_STATE, action) { switch (action.type) { case "CART_INC_QNT": return { ...state, wait: true }; case "CART_INC_QNT_SUCCESS": return { ...state, qnt: action.payload, wait: false }; case "CART_LOAD": return { ...state, qnt: action.payload }; default: return state; }
}

Доработаем контейнер CartPage:

// src/containers/cart-page.js render() { const { incQnt, load } = this.props.actions; const { qnt, wait } = this.props.cart; return ( <div className={"cart-page"}> <CartInfo qnt={qnt} onLoad={load} />{" "} <Cart onIncQnt={incQnt} wait={wait} /> </div> );
}

Доработаем компонент Cart:

// src/components/cart.js render() { return ( <button type="button" onClick={this.props.onIncQnt} disabled={this.props.wait} > {this.props.wait ? "Wait..." : "Increment"} </button> );
}

И компонент CartInfo:

// src/components/cart-info.js import React, { Component, Fragment } from "react"; class CartInfo extends Component { componentDidMount() { this.props.onLoad(); } render() { return <Fragment>Quantity: {this.props.qnt}</Fragment>; }
} export default CartInfo;

=) Такие дела.

Вариант третий. MVVM с Observers

Демо и исходный код:

Начнем с принципа работы MVVM:

MVVM principles

Model, как чаще всего это понимают, это не только какой-то класс с набором данных, которые ничего не делают. Как я и сказал в первом варианте, принцип работы у них одинаковый: есть какой-то условный "контейнер" (View Model), который общается между View и всей бизнес-логикой (Model). И частая здесь ошибка при реализации, это когда View имеет прямой доступ к Model минуя View Model. Под model подразумевается бизнес-логика. View ничего не знает и никогда в глаза не видела Model, View знает только о View Model и все манипуляции с данными должны выполняться исключительно через View Model. Это в корне неверно.

Первое, что необходимо сделать, это написать нашу модель. Что-ж, приступим к реализации. В нашем случае все просто, это одно свойство qnt.

// src/models/cart.js class Cart { constructor() { this._qnt = 0; } getQnt(qnt) { this._qnt = qnt; } setQnt() { return this._qnt; }
} export default Cart;

Здесь нам поможет паттерн Observer. Далее наступает самое интересное: нам нужно, чтобы как-то данные между собой синхронизировались согласно принципу работы MVVM. Напишем наш View Model: Его мы будем использовать, чтобы дать понять View, когда перерисовываться.

// src/view-models/cart.js class CartViewModel { constructor(model) { this._subscribers = []; this._model = model; this.incQnt = this.incQnt.bind(this); } getModel() { return this._model; } incQnt() { this._model.setQnt(this._model.getQnt() + 1); this.notifyChange(); } subscribeOnChange(handler) { this._subscribers.push(handler); } unsubscribeOnChange(handler) { if (handler === undefined) { this._subscribers = []; } else { this._subscribers = this._subscribers.filter( subscriber => subscriber !== handler ); } } notifyChange() { for (let i = 0; i !== this._subscribers.length; i++) { this._subscribers[i](); } }
} export default CartViewModel;

Исходя из кода выше мы видим:

  • метод getModel(), который предназначен для доступа к данным модели (доступ к данным из модели можно/нужно/необходимо сделать ограниченным и сделать доступными только нужные свойства, а не открывать всю модель);
  • метод subscribeOnChange() который служит для того, чтобы подписаться на изменения;
  • метод unsubscribeOnChange(), чтобы отписаться от изменений;
  • и метод notifyChange(), который необходим для оповещения всех подписчиков о изменениях.

Теперь, доработаем наш CartPage контейнер:

// src/containers/cart-page.js import React, { Component } from "react";
import Cart from "./../components/cart";
import CartInfo from "./../components/cart-info";
import CartViewModel from "./../view-models/cart";
import CartModel from "./../models/cart"; class CartPage extends Component { constructor(props) { super(props); this._viewModel = new CartViewModel(new CartModel()); this._changeHandler = this._changeHandler.bind(this); } componentDidMount() { this._viewModel.subscribeOnChange(this._changeHandler); } componentWillUnmount() { this._viewModel.unsubscribeOnChange(this._changeHandler); } render() { const model = this._viewModel.getModel(); const { incQnt } = this._viewModel; return ( <div className={"cart-page"}> <CartInfo qnt={model.getQnt()} /> <Cart onIncQnt={incQnt} /> </div> ); } _changeHandler() { this.forceUpdate(); }
} export default CartPage;

Здесь не особо что-то изменилось, мы всего лишь слушаем изменения и перерисовываем View.

Давайте это исправим и реализуем базовый класс для View Model и HOC для контейнера. И все вроде ничего, но получается, что нам постоянно придется писать одинаковый код (бойлерплейт), как в View Model, так и в нашем контейнере.

Вынесем все, что связано с наблюдателем в базовый класс View Model:

// src/view-models/base-view-model.js class BaseViewModel { constructor() { this._subscribers = []; } subscribeOnChange(handler) { this._subscribers.push(handler); } unsubscribeOnChange(handler) { if (handler === undefined) { this._subscribers = []; } else { this._subscribers = this._subscribers.filter( subscriber => subscriber !== handler ); } } notifyChange() { for (let i = 0; i !== this._subscribers.length; i++) { this._subscribers[i](); } }
} export default BaseViewModel;

После этого наш CartViewModel примет следующий вид:

// src/view-models/cart.js import BaseViewModel from "./base-view-model"; class CartViewModel extends BaseViewModel { constructor(model) { super(); this._model = model; this.incQnt = this.incQnt.bind(this); } getModel() { return this._model; } incQnt() { this._model.setQnt(this._model.getQnt() + 1); this.notifyChange(); }
} export default CartViewModel;

Осталось написать HOC, где будет автоматически происходить подписка на изменения в View Model:

// src/hoc/with-observer.js import React, { PureComponent } from "react"; export function withObserver(WrappedComponent, viewModel) { return class extends PureComponent { constructor(props) { super(props); this._viewModel = viewModel; this._changeHandler = this._changeHandler.bind(this); } componentDidMount() { this._viewModel.subscribeOnChange(this._changeHandler); } componentWillUnmount() { this._viewModel.unsubscribeOnChange(this._changeHandler); } render() { return <WrappedComponent viewModel={this._viewModel} {...this.props} />; } _changeHandler() { this.forceUpdate(); } };
}

В след за этими изменениями модифицируем наш контейнер CartPage:

// src/containers/cart-page.js import React, { Component } from "react";
import Cart from "./../components/cart";
import CartInfo from "./../components/cart-info";
import CartViewModel from "./../view-models/cart";
import { withObserver } from "./../hoc/with-observer";
import CartModel from "./../models/cart"; class CartPage extends Component { render() { const model = this.props.viewModel.getModel(); const { incQnt } = this.props.viewModel; return ( <div className={"cart-page"}> <CartInfo qnt={model.getQnt()} /> <Cart onIncQnt={incQnt} /> </div> ); }
} export default withObserver(CartPage, new CartViewModel(new CartModel()));

И никакого обилия лишних библиотек и костылей, все нативно. Вот и все. Что может быть проще?

Да, действительно. Внимательный читатель снова заметит, "а как же API!?". Вернем обратно наш репозиторий: Давайте добавим API.

Добавим репозиторий cart:

// src/repositories/cart.js let fakeServerState = { qnt: 0
}; export default { getQnt, incQnt
}; export function getQnt() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(fakeServerState.qnt); }, 1000); });
} export function incQnt() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(++fakeServerState.qnt); }, 1000); });
}

Доработаем модель Cart:

// src/models/cart.js class Cart { constructor() { this._qnt = 0; this._wait = false; } setQnt(qnt) { this._qnt = qnt; } getQnt() { return this._qnt; } getWait() { return this._wait; } setWait(wait) { this._wait = wait; }
} export default Cart;

Доработаем CartViewModel:

// src/view-models/cart.js import BaseViewModel from "./base-view-model";
import cartRepository from "./../repositories/cart"; class CartViewModel extends BaseViewModel { constructor(model) { super(); this._model = model; this.load = this.load.bind(this); this.incQnt = this.incQnt.bind(this); } getModel() { return this._model; } load() { this._model.setWait(true); this.notifyChange(); cartRepository.getQnt().then(payload => { this._model.setQnt(payload); this._model.setWait(false); this.notifyChange(); }); } incQnt() { this._model.setWait(true); this.notifyChange(); cartRepository.incQnt().then(payload => { this._model.setQnt(payload); this._model.setWait(false); this.notifyChange(); }); }
} export default CartViewModel;

Немного изменим CartPage:

// src/containers/cart-page.js render() { const model = this.props.viewModel.getModel(); const { incQnt, load } = this.props.viewModel; return ( <div className={"cart-page"}> <CartInfo qnt={model.getQnt()} onLoad={load} />{" "} <Cart onIncQnt={incQnt} wait={model.getWait()} /> </div> );
}

И компонент Cart с CartInfo:

// src/components/cart.js render() { return ( <button type="button" onClick={this.props.onIncQnt} disabled={this.props.wait} > {this.props.wait ? "Wait..." : "Increment"} </button> );
}

// src/components/cart-info.js componentDidMount() { this.props.onLoad();
}

=) Теперь у нас есть API.

Тогда, нам на помощь приходит тот же самый паттерн Observer и плюс какой-нибудь сервис (служба), которая будет отвечать за перерасчет количества. И, наконец, остался последний вопрос, "а что, если эти два компонента лежат не в одном контейнере, а в нескольких?".

Напишем наш сервис cart:

// src/services/cart.js import cartRepository from "./../repositories/cart"; let subscribers = []; export default { incQnt, getQnt, subscribeOnIncQnt, unsubscribeOnIncQnt
}; export function incQnt() { return cartRepository.incQnt().then(payload => { notifyIncQnt(payload); return payload; });
} export function getQnt() { return cartRepository.getQnt().then(payload => { notifyIncQnt(payload); return payload; });
} export function subscribeOnIncQnt(handler) { subscribers.push(handler);
} export function unsubscribeOnIncQnt(handler) { if (handler === undefined) { subscribers = []; } else { subscribers = subscribers.filter( subscriber => subscriber !== handler ); }
} function notifyIncQnt(qnt) { for (let i = 0; i !== subscribers.length; i++) { subscribers[i](qnt); }
}

Теперь он взаимодействует с сервисом cart: Далее, подправим наш CartViewModel.

// src/view-models/cart.js import BaseViewModel from "./base-view-model";
import cartService from "./../services/cart"; class CartViewModel extends BaseViewModel { constructor(model) { super(); this._model = model; this.incQnt = this.incQnt.bind(this); } getModel() { return this._model; } incQnt() { this._model.setWait(true); this.notifyChange(); cartService.incQnt().then(payload => { this._model.setWait(false); this.notifyChange(); }); }
} export default CartViewModel;

А CartInfoViewModel слушает все изменения, которые могут произойти в сервисе cart:

// src/view-models/cart-info.js import BaseViewModel from "./base-view-model";
import cartService from "./../services/cart"; class CartInfoViewModel extends BaseViewModel { constructor(model) { super(); this._model = model; this.load = this.load.bind(this); cartService.subscribeOnIncQnt(payload => { this._model.setQnt(payload); this.notifyChange(); }); } getModel() { return this._model; } load() { cartService.getQnt(); }
} export default CartInfoViewModel;

Доработаем контейнер CartPage:

// src/containers/cart-page.js render() { const model = this.props.viewModel.getModel(); const { incQnt } = this.props.viewModel; return ( <div className={"cart-page"}> <Cart onIncQnt={incQnt} wait={model.getWait()} /> </div> );
}

И StatsSection, где у нас теперь лежит компонент CartInfo:

// src/containers/stats-section.js import React, { Component } from "react";
import CartInfo from "./../components/cart-info";
import { withObserver } from "./../hoc/with-observer";
import CartInfoViewModel from "./../view-models/cart-info";
import CartInfoModel from "./../models/cart-info"; class StatsSection extends Component { render() { const model = this.props.viewModel.getModel(); const { load } = this.props.viewModel; return ( <div className={"stats-section"}> <CartInfo qnt={model.getQnt()} onLoad={load} /> </div> ); }
} export default withObserver( StatsSection, new CartInfoViewModel(new CartInfoModel())
);

А точку входа затронут незначительные изменения:

// src/index.js import React, { PureComponent } from "react";
import ReactDOM from "react-dom";
import CartPage from "./containers/cart-page";
import StatsSection from "./containers/stats-section"; class App extends PureComponent { render() { return ( <div className={"app"}> <StatsSection /> <CartPage /> </div> ); }
} ReactDOM.render(<App />, document.getElementById("root"));

С этого момента два компонента, находящиеся в разных контейнерах, но зависящих один от другого, взаимодействуют между собой. На этом все.

Заключение

  1. Как вы могли заметить, можно вполне жить без Redux и писать расширяемые приложения.
  2. Данная статья не является строгой инструкцией к действию, она лишь описывает возможные принципы работы с React (и другими любыми библиотеками реализующие часть View), которые можно и нужно расширять под потребности.
  3. Да, необязательно оповещать об изменениях в View Model только лишь используя один метод notifyChange(), можно реализовать большее количество различных Subject, например, на каждое свойство.
  4. Да, вы можете не писать свои реализации взаимодействия с событиями, а воспользоваться чем-то готовым, вроде RxJS или Event Emitter.
  5. Да, можете использовать MobX/MVC/Flux/etc.

Ссылки

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»