Вам не нужен Redux
Очередная статья, которая, возможно, так и останется в черновиках, но если вы это читаете, то все-таки это свершилось.
Благо, архитектура приложения позволяла его выпилить безболезненно. К написанию статьи послужил опыт с Redux
, потому что повестись на хайп было опрометчивым решением. =)
Почему-то эта опьяняющая очевидность ко мне пришла далеко спустя время, хотя я имею опыт в написании приложений под Android
, где никакого Redux
нет и все живы, и здоровы. Долгое время и огромное количество человек я расспрашивал про то, как они используют Redux
и всегда удивлялся.
К примеру, в нашем проекте Redux
оправдывает себя только в нескольких местах, остальное можно реализовать тупо на Dumb & Smart Components
. Вся проблема в том, что никто не объясняет зачем нужен и когда нужен Redux
, пока ты не наступил на эти грабли спустя время. Если в приложении мало сепаратных частей, которые друг на друге могли бы быть зависимы, то это маловыгодная вещь. И в тех самых местах, где он оправдан, я бы также его убрал и использовал события. Но люди, почему-то, ссут подумать своей головой. Отсюда и появляются всякие оптимизационные костыли (reselect
, например). И сейчас я вам поведаю сказ о том, как жить без Redux
.
Моделирование ситуации
Эта ситуация, когда мы имеем два сепаратных компонента, где изменение в одном компоненте должно затрагивать изменение второго. Я не буду далеко ходить и возьму самую простую ситуацию, которую можно легко понять и быстро реализовать. Дабы уменьшить код и не писать компонент списка товаров я предлагаю просто реализовать в одном месте отображение количества, а во втором кнопку инкремента. Например, счетчик количества товара в корзине (первый компонент CartInfo
) и список товаров в таблице (второй компонент Cart
). Схематично это должно выглядеть так:
мы видим, что компоненты между собой никак не связаны. Т.е. 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
или что-то другое. Если нет никакой сложной бизнес-логики (а чаще всего оно так и есть), то решение идеальное. Более подробно можно подчерпнуть здесь. Это, пожалуй, самое первое, что нужно изучить.
Принцип работы:
=)). Эта не самая простая схема принципа работы, но единственная в интернете, которая отражает реальность вещей (самому рисовать мне лень. Если закрыть глаза на слово "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
;
А если еще и оптимизацией заниматься в будущем, потому что иначе никак, то… ууу… целый квест:
Но, перейдем к сути и не будем говорить о жопочасах.
Принцип работы:
Добавим редьюсер 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:
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"));
С этого момента два компонента, находящиеся в разных контейнерах, но зависящих один от другого, взаимодействуют между собой. На этом все.
Заключение
- Как вы могли заметить, можно вполне жить без
Redux
и писать расширяемые приложения. - Данная статья не является строгой инструкцией к действию, она лишь описывает возможные принципы работы с
React
(и другими любыми библиотеками реализующие частьView
), которые можно и нужно расширять под потребности. - Да, необязательно оповещать об изменениях в
View Model
только лишь используя один методnotifyChange()
, можно реализовать большее количество различныхSubject
, например, на каждое свойство. - Да, вы можете не писать свои реализации взаимодействия с событиями, а воспользоваться чем-то готовым, вроде
RxJS
илиEvent Emitter
. - Да, можете использовать
MobX
/MVC/Flux/etc.