Доброго времени суток.
Занимаюсь разработкой проекта на React и Redux. Хочу в этой статье описать архитектуру своего проекта.
Итак, начнем. Файловая структура:
Для подключения редьюсеров создаем класс singleton reducerRegister:
./reducerRegister.js
class ReducerRegistry {
constructor () {
if (!ReducerRegistry.instance) {
this._emitChange = null
this._reducers = {}
ReducerRegistry.instance = this
}
return ReducerRegistry.instance
}
getReducers () {
return {...this._reducers}
}
register (name, reducer) {
this._reducers = {...this._reducers, [name]: reducer}
if (this._emitChange) {
this._emitChange(this.getReducers())
}
}
setChangeListener (listner) {
this._emitChange = listner
}
}
const reducerRegistry = new ReducerRegistry()
export default reducerRegistry
С помощью этого класса редьюсеры могут сами себя регистрировать в store.
Создаем store:
./configureStore
export default function configureStore (initialState) {
const combine = (reducers) => {
const reducerNames = Object.keys(reducers)
Object.keys(initialState).forEach(item => {
if (reducerNames.indexOf(item) === -1) {
reducers[item] = (state = null) => state
}
})
reducers['router'] = connectRouter(history)
return combineReducers(reducers)
}
const reducer = combine(reducerRegistry.getReducers())
const store = createStore(reducer, initialState, compose(composeWithDevTools(applyMiddleware(thunk)), applyMiddleware(routerMiddleware(history))))
reducerRegistry.setChangeListener(reducers => {
store.replaceReducer(combine(reducers))
})
return store
}
С помощью функции store.replaceReducer загружаем редьюсеры в store.
Основной файл
Добавляем маршруты и подключаем redux
./index.js
const Cabinet = React.lazy(() => import('./moduleCabinet/Index'))
let store = configureStore({
profile: {loading: null}
})
class App extends Component {
render () {
const history = createBrowserHistory()
return (
<Router basename="/">
<ConnectedRouter history={history}>
<Suspense fallback={<Loader/>}>
<Switch>
<Route exact path="/" component={Main} />
<Route path="/admin" render={(props) => <RouteAdmin {...props} />}/>
<Route path="/cabinet" component={props => <Cabinet {...props} />}}/>
<Route
path="/"
component={() => <div>page not found</div>}
/>
</Switch>
</Suspense>
</ConnectedRouter>
</Router>
)
}
}
if (document.getElementById('app')) {
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('app')
)
}
С помощью React.lazy делаем ленивую загрузку компонентов.React.lazy доступен, начиная с версии 16.6: React. Lazy loading. В элементе Suspense обрабатывается загрузка компонента.
AdminModule может загрузить только авторизованный пользователь, для этого используем компонент RouteAdmin:
./RouteAdmin.js
const NotAccess = (props) => {
return (
<div>
<h1 className="text-danger">Доступ закрыт</h1>
</div>
)
}
export default class RouteAdmin extends Component {
constructor (props) {
super(props)
this.state = {
component: null
}
}
componentDidMount () {
axios.post('/admin').then(data => data.data).then(data => {
if (data.auth === true) {
const Admin = React.lazy(() => import('./moduleAdmin/Index'))
this.setState({component: Admin})
} else {
this.setState({component: NotAccess})
}
})
}
render () {
const Component = this.state.component
return (
<Route path="/admin" {...this.props} render={this.state.component}/>
)
}
}
Реализация модуля
Основной файл — добавляем маршруты модуля
./moduleAdmin/Index.js
export default class IndexComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
<>
<Route to="/admin/Profiles" component={Profiles} />
...
</>
)
}
}
./moduleAdmin/pages/Profiles.js
class Profiles extends Component {
componentDidMount() {
this.props.getInfo()
}
render() {
if (this.props.loading === Process.Start) {
return <Loader />
}
if (this.props.loading === Process.Success) {
return (
<div>
<h1>Profiles</h1>
</div>
)
}
return null
}
}
const mapStateToProps = (state) => {
return {
loading: state.profiles.loading
}
}
const mapDispatchToProps = (dispatch) => {
return {
getInfo: () => dispatch(getInfo())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Profiles)
Создаем редьюсер
Тут же регистрируем его в store:
./moduleAdmin/redux/profile.js
const Process = {
Start: 0, Success: 1, Error: 2
}
export const getInfo = () => {
return (dispatch) => {
dispatch({ type: PROFILES_GET_START })
axios.post('/news').then((data) => {
dispatch({ type: PROFILES_GET_SUCCESS, payload: data.data })
}).catch(e => {
dispatch({ type: PROFILES_GET_ERROR, payload: e })
})
}
}
const initialState = {
error: null, loading: null, data: null
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case PROFILES_GET_START: {
return { ...state, loading: Process.Start }
}
case PROFILES_GET_SUCCESS: {
return { ...state, loading: Process.Success, data: action.payload}
}
case PROFILES_GET_ERROR: {
return { ...state, loading: Process.Error, error: action.payload }
}
default: {
return state
}
}
}
reducerRegistry.register('profiles', reducer)
Надеюсь моя статья поможет в реализации вашего проекта, а ваши комментарии помогут улучшить мой.
Комментарии (15)
Frozik
24.05.2019 13:50По мне все круто, только для динамической загрузки редьюсеров и саг я бы заюзал microsoft/redux-dynamic-modules
Asrover
24.05.2019 14:42В своих проектах стараюсь использовать компоненты-контейнеры, которые отвечают за логику получения данных, а также презентационные компоненты, которые получают необходимые данные из контейнера и отображают их. Такой подход позволяет разделить ответственность и код становится более читабельным. Также одни и те же данные могут понадобиться в нескольких местах, например получение списка новостей для страницы «Новости» и для компонента «Последние новости», который скажем показывается на «Главной странице». Компонент-контейнер в таком случае позволит избежать дублирования кода.
dizel3d
25.05.2019 13:47Имхо, если проект еще не ушел в тяжелое легаси, лучше разрюхать react new context api и react hooks, а не городить очередной костыль для redux.
А еще рекомендую использовать typescript.
JustDont
Сколько не смотрю на весь этот набитый бойлерплейтом редакс, а никогда не мог понять глубинную логику разделения на action и reducer. Идеология выглядит красиво — мол, экшены фигачат сообщения вида «что случилось», а редьюсеры уже стейт правят. На практике же у нас одна ответственность вида «обновить стейт определенным образом» (на этом всё остальное приложение держится, собственно, ожидая, что стейт будет обновляться определенными способами) поделилась на два несвязанных куска — экшн, который сам по себе это ничего не значащая тыква, и редьюсер, который сам по себе тоже ничего не значащая тыква, но если она возвращает что-то интересное в ответ на определенные строки в action.type, то всё это наконец-то цепляется друг за друга и начинает вертеться.
Вот только почему это всегда выглядит очень лишним усложнением на ровном месте?
mayorovp
Это всё имеет смысл, если один action обрабатывается несколькими редьюсерами, прямо или косвенно (через мидлвари).
JustDont
Конечно. Но смотрите: совсем посторонние сайд-эффекты (мидлварь) всегда можно как-то организованно дёрнуть из приложения, место для такого дерганья как правило легко находится или организовывается, редакс у вас или не редакс.
Внутренняя же логика приложения из нескольких редьюсеров — это вы просто берете и очевидную ветвящуюся логику (если А, делаем B+C, иначе делаем B+D) пихаете куда-то в гораздо менее очевидное место (коллекция редьюсеров в сторе на данный момент). Проще говоря, меняете очевидный код на неочевидный — было ветвление по действиям, стало ветвление для разнообразного подписывания редьюсеров в стор. Что само по себе всегда очень плохо.
VolCh
Если я правильно понял ваш комментарий (несколько раз перечитал), то ваш очевидный код является сильно связанным. Какой-то простой компонент типа кнопки должен знать про A, B, C и D.
JustDont
Конечно нет. Зачем про это знать компонентам?
Если у нас есть стор и его обновления (неважно, редакс это или не редакс), и архитектура более-менее MVC (вернее, обычно таки MVP или MVVM, но это в общем-то одни и те же яйца с разных ракурсов), то сфигали логика будет прибита к компонентам? Вы не представляете других мест, куда её можно положить, что ли?
Всё, что я описал — это вариант, в котором вместо двух раздельных «делаем действие» и «редьюсеры обновляют стор» происходит только одно «делаем действие» (с необходимыми ветвлениями внутри).
VolCh
Ну вот простой компонент был с ответственностью вызвать изменение стора B при нажатии мышью на кнопку. пришло новое требование "если нажата левая кнопка, то дополнительно вызывать C, если правая, то дополнительно вызывать D". Где вы будете это делать?
В сторах подобных редаксовым, с разделениями на экшены и редюсеры (события и их обработчики, по сути) мы просто добавим новый обработчик для существующего экшена, а может даже два новых обработчика, которые будут игнорировать не свою кнопку. Без разделения, как я понимаю, в сторе был метод B (причём B скорее всего что-то из предметной области, ане onMyButtonClick, который вызывался в onClick. Теперь нам нужно будет создавать именно onMyButtonClick скорее всего, в котором анализировать какая кнопка нажата. Ну или создавать метод типа doBwithCorD(bool withC, bool withD) и передавать соотвествующие флаги. Так?
JustDont
Если «новое требование» относится к изменению предметной области (то есть, действия остались теми же самыми, только их суть теперь другая) — то код компонента вообще не меняется, в нем так и остаётся «метод В», только «метод В» теперь будет работать иначе. Это, конечно, всё зависит от уровня охвата — для цельного SPA скорее всего будет не так (а для какого-то компонентного куска про кнопки и нажатия на них — возможно).
Если же «новое требование» к предметной области отношения не имеет — в компонент (в обработчик onClick, если точнее) вносится логика по вызову C или D в зависимости от условий. Если компонент один — то прямо в него, почему бы и нет. Если компонентов много — куда-то в общее для этих компонентов место. Это ровно то же самое, что вы сделаете, если «просто добавите новый обработчик или два», только явно изложенное в коде, а не зарытое во внутренней логике редьюсеров и в строках, подписывающих их в стор.
В парадигме action-reducer, если вам по прежнему надо где-то «просто» нажимать на кнопки, а где-то с вывертом — вы скорее всего будете заводить новый тип action «с вывертами», и семантически это будет то же самое, что и onMyButtonClick. Ну и опять же вам придётся написать код, который позаботится о том, что в action будет положена нажатая кнопка, чтоб потом редьюсеру было, что проверять. При этом у вас нет никакой особой свободы творчества, и вы даже так просто не дернете подряд два экшна из onClick (в духе doSimpleClick(); doComplexClick(ev)) — для этого вам сначала придётся пойти и разобраться, нет ли в них какой-нибудь асинхронности, которая может породить нежелательные обновления UI, если отработает в произвольном порядке.
VolCh
Я скорее про ситуацию, когда кнопка вызывала какие-то изменения в модели предметной области, а новые требования говорят, что дополнительно к этим изменениям в модели нужно делать одно из двух других в зависимости от условия какого-то. Чисто UI задача для конкретного компонента — уменьшение количество кликов пользователем в какой-то ситуации. И я не буду заводить отдельный экшен, просто добавлю поле в существующий. Аналогично вызову метода с параметром.
JustDont
… тем самым еще больше скрывая и запутывая логику. I rest my case. Во имя архитектуры нам для выяснения логики работы конкретного действия теперь надо быть в курсе подписок редьюсеров в данный момент рантайма (хорошо, если оно там особо не пляшет в рантайме, а более-менее единожды задаётся) и в курсе полного содержимого конкретного action. Ура?
PS: И не поймите меня неправильно, я не считаю такой подход чем-то плохим самим по себе. Это работает, это пишется, и приемлемо читается, если вы прониклись парадигмой action-reducer. Ну или дебажится в рантайме, в крайнем случае. Но это очень далеко не идеал.
staticlab
Вы всегда можете хранить всё состояние приложения в корневом компоненте, откуда пробрасывать стейт и изменяющие методы через контекст в требуемые компоненты. Это нетрудно сделать на чистом Реакте.
staticlab
Набросал вот такой небольшой концепт, но в нём есть проблемы с масштабируемостью: https://github.com/artptr/todoapp-recon/tree/master/src/app. Нужно ещё думать, как их лучше решить.
JustDont
Ну да, это примерно то, что вменяемый человек будет колхозить для реализации MVVM в реакте, если вдруг по каким-то причинам оно ему надо будет.
Другое дело, что я не уверен, что оно кому-то особо надо. Я пока не разубеждён в том, что для реализации хорошей M и управления ей надо использовать что-то отличное от реактивного программирования. С реактивным программированием обычно выходит всё очень чисто и просто для понимания (когда есть представление, как вообще реактивное программирование устроено). В идеале всю модель вообще можно свести к псевдо-POJO, с которым вся работа проходит через банальные геттеры и сеттеры, но у которого под капотом этого завёрнута вся логика.
Тяжелый момент, как обычно, будет в том, как бы эту модель с логикой прицепить к рендеру без костылей. У реакта есть определенные проблемы в этом плане (например, MobX их решил, но костылями).