Магазин Redux Против Состояния React

Как спроектировать хранилище данных в приложении React? Где хранить данные приложения: в глобальном хранилище (хранилище Redux) или в локальном хранилище (состояние компонента)?

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

За 5 лет разработки на React мы в BENOVATE опробовали на практике различные подходы к построению архитектуры подобных приложений.

В этой статье мы рассмотрим возможные критерии выбора места хранения данных в приложении.

А может вообще без Redux? Да, если можно обойтись без этого.

По этой теме вы можете прочитать статья от одного из создателей библиотеки Дэна Абрамова.

Если разработчик понимает, что без Redux не обойтись, то можно выделить несколько критериев выбора хранилища данных:

  1. Срок службы данных
  2. Частота использования
  3. Возможность отслеживать изменения состояния


Срок службы данных

Есть 2 категории:
  • Часто меняющиеся данные.

  • Редко меняющиеся данные.

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



Часто меняющиеся данные



К этой категории относятся, например, параметры фильтрации, сортировки и навигации по страницам компонента, реализующего работу со списком объектов, или флаг, отвечающий за отображение отдельных элементов пользовательского интерфейса в приложении, например, выпадающего списка или модального окна.

окно (при условии, что оно не привязано к пользовательским настройкам).

Сюда также входят данные формы, которые необходимо заполнить до момента отправки на сервер.

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

Плохой пример

 
 
 
 
 
 
 
 
  import React from 'react';

import { connect } from 'react-redux';

import { toggleModal } from '.

/actions/simpleAction' import logo from '.

/logo.svg'; import '.

/App.css'; import Modal from '.



/elements/modal';

const App = ({

openModal,

toggleModal,

}) => {

return (

<div className="App">

<header className="App-header">

<img src={logo} className="App-logo" alt="логотип" />

</header>

<main className="Main">

<button onClick={() => toggleModal(true)}>{'Open Modal'}</button>

</main>

<Modal isOpen={openModal} onClose={() => toggleModal(false)} />

</div>

);

}

const mapStateToProps = (state) => {

return {

openModal: state.simple.openModal,

}

}

const mapDispatchToProps = { toggleModal }

export default connect(

mapStateToProps,

mapDispatchToProps

)(App)

// src/constants/simpleConstants.js

export const simpleConstants = {

TOGGLE_MODAL: 'SIMPLE_TOGGLE_MODAL',

};

// src/actions/simpleAction.js

import { simpleConstants} from ".

/constants/simpleConstants"; export const toggleModal = (open) => ( { type: simpleConstants.TOGGLE_MODAL, payload: open, } ); // src/reducers/simple/simpleReducer.js import { simpleConstants } from ".

/.

/constants/simpleConstants"; const initialState = { openModal: false, }; export function simpleReducer(state = initialState, action) { switch (action.type) { case simpleConstants.TOGGLE_MODAL: return { .

state, openModal: action.payload, }; default: return state; } }

Хороший пример import React, {useState} from 'react'; import logo from '.

/logo.svg'; import '.

/App.css'; import Modal from '.



/elements/modal';

const App = () => {

const [openModal, setOpenModal] = useState(false);

return (

<div className="App">

<header className="App-header">

<img src={logo} className="App-logo" alt="логотип" />

</header>

<main className="Main">

<button onClick={() => setOpenModal(true)}>{'Open Modal'}</button>

</main>

<Modal isOpen={openModal} onClose={() => setOpenModal(false)} />

</div>

);

}

export default App;



Редко меняющиеся данные

Это данные, которые обычно не изменяются между обновлениями страницы или между посещениями страницы отдельными пользователями.

Поскольку хранилище Redux воссоздается при обновлении страницы, данные этого типа должны храниться где-то еще: в базе данных на сервере или в локальном хранилище браузера.

Это могут быть данные каталога или пользовательские настройки.

Например, при разработке приложения, использующего пользовательские настройки, после аутентификации пользователя мы сохраняем эти настройки в магазине Redux, что позволяет компонентам приложения использовать их без обращения к серверу.

Стоит помнить, что некоторые данные могут измениться на сервере без взаимодействия с пользователем, и вам нужно продумать, как на это отреагирует ваше приложение.

Плохой пример // App.js import React from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import ProfileEditForm from '.



/elements/profileeditform';

const App = () => {

return (

<div className="App">

<Header />

<main className="Main">

<ProfileEditForm />

</main>

</div>

);

}

export default App;

// src/elements/header.js

import React from "react";

import logo from ".

/logo.svg"; import Menu from ".

/menu"; export default () => ( <header className="App-header"> <img src={logo} className="App-logo" alt="логотип" /> <Menu /> </header> ) // src/elements/menu.js import React, {useEffect, useState} from "react"; import { getUserInfo } from '.

/api'; const Menu = () => { const [userInfo, setUserInfo] = useState({}); useEffect(() => { getUserInfo().



then(data => {

setUserInfo(data);

});

}, []);

return (

<>

<span>{userInfo.userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

}

export default Menu;

// src/elements/profileeditform.js

import React, {useEffect, useState} from "react";

import {getUserInfo} from ".

/api"; const ProfileEditForm = () => { const [state, setState] = useState({ isLoading: true, userName: null, }) const setName = (e) => { const userName = e.target.value; setState(state => ({ .

state, userName, })); } useEffect(() => { getUserInfo().

then(data => { setState(state => ({ .



state,

isLoading: false,

userName: data.userName,

}));

});

}, []);

if (state.isLoading) {

return null;

}

return (

<form>

<input type="text" value={state.userName} onChange={setName} />

<button>{'Save'}</button>

</form>

)

}

export default ProfileEditForm;

Хороший пример // App.js import React, {useEffect} from 'react'; import {connect} from "react-redux"; import '.

/App.css'; import Header from '.

/elements/header'; import ProfileEditForm from '.

/elements/profileeditform'; import {loadUserInfo} from ".



/actions/userAction";

const App = ({ loadUserInfo }) => {

useEffect(() => {

loadUserInfo()

}, [])

return (

<div className="App">

<Header />

<main className="Main">

<ProfileEditForm />

</main>

</div>

);

}

export default connect(

null,

{ loadUserInfo },

)(App);

// src/elements/header.js

import React from "react";

import logo from ".

/logo.svg"; import Menu from ".



/menu";

export default () => (

<header className="App-header">

<img src={logo} className="App-logo" alt="логотип" />

<Menu />

</header>

)

// src/elements/menu.js

import React from "react";

import { connect } from "react-redux";

const Menu = ({userName}) => (

<>

<span>{userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

const mapStateToProps = (state) => {

return {

userName: state.userInfo.userName,

}

}

export default connect(

mapStateToProps,

)(Menu);

// src/elements/profileeditform.js

import React from "react";

import { changeUserName } from '.



/actions/userAction'

import {connect} from "react-redux";

const ProfileEditForm = ({userName, changeUserName}) => {

const handleChange = (e) => {

changeUserName(e.target.value);

};

return (

<form>

<input type="text" value={userName} onChange={handleChange} />

<button>{'Save'}</button>

</form>

)

}

const mapStateToProps = (state) => {

return {

userName: state.userInfo.userName,

}

}

const mapDispatchToProps = { changeUserName }

export default connect(

mapStateToProps,

mapDispatchToProps,

)(ProfileEditForm);

// src/constants/userConstants.js

export const userConstants = {

SET_USER_INFO: 'USER_SET_USER_INFO',

SET_USER_NAME: 'USER_SET_USER_NAME',

UNDO: 'USER_UNDO',

REDO: 'USER_REDO',

};

// src/actions/userAction.js

import { userConstants } from ".

/constants/userConstants"; import { getUserInfo } from ".



/api/index";

export const changeUserName = (userName) => (

{

type: userConstants.SET_USER_NAME,

payload: userName,

}

);

export const setUserInfo = (data) => (

{

type: userConstants.SET_USER_INFO,

payload: data,

}

)

export const loadUserInfo = () => async (dispatch) => {

const result = await getUserInfo();

dispatch(setUserInfo(result));

}

// src/reducers/user/userReducer.js

import { userConstants } from ".

/.

/constants/userConstants"; const initialState = { userName: null, }; export function userReducer(state = initialState, action) { switch (action.type) { case userConstants.SET_USER_INFO: return { .

state, .

action.payload, }; case userConstants.SET_USER_NAME: return { .

state, userName: action.payload, }; default: return state; } }



Частота использования

Второй критерий — сколько компонентов в приложении React должно иметь доступ к одному и тому же состоянию.



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

Если вы понимаете, что для определенного компонента или небольшой части вашего приложения состояние изолировано, то лучше использовать React-состояние отдельного компонента или HOC-компонента.





Государственная глубина передачи

В приложениях, отличных от Redux, данные состояния React должны храниться в самом верхнем (в дереве) компоненте, дочерним компонентам которого потребуется доступ к этим данным, при условии, что мы избегаем хранения одних и тех же данных в разных местах.

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

вы обнаружите, что дочернему компоненту необходим доступ к новым данным о состоянии.

В таких случаях разумнее хранить состояние в Redux и извлекать необходимые данные из хранилища в соответствующих компонентах.

Если вам нужно передать данные о состоянии дочерним компонентам на одном или двух уровнях вложенности, то вы можете сделать это без Redux. Плохой пример //App.js import React from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import MainContent from '.

/elements/maincontent'; const App = ({userName}) => { return ( <div className="App"> <Header userName={userName} /> <main className="Main"> <MainContent /> </main> </div> ); } export default App; // .

/elements/header.js import React from "react"; import logo from ".

/logo.svg"; import Menu from ".

/menu"; export default ({ userName }) => ( <header className="App-header"> <img src={logo} className="App-logo" alt="логотип" /> <Menu userName={userName} /> </header> ) // .



/elements/menu.js

import React from "react";

export default ({userName}) => (

<>

<span>{userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

Хороший пример // App.js import React from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import MainContent from '.

/elements/maincontent'; const App = () => { return ( <div className="App"> <Header /> <main className="Main"> <MainContent /> </main> </div> ); } export default App; //.

/elements/header.js import React from "react"; import logo from ".

/logo.svg"; import Menu from ".

/menu"; export default () => ( <header className="App-header"> <img src={logo} className="App-logo" alt="логотип" /> <Menu /> </header> ) //.



/elements/menu.js

import React from "react";

import { connect } from "react-redux";

const Menu = ({userName}) => (

<>

<span>{userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

const mapStateToProps = (state) => {

return {

userName: state.userInfo.userName,

}

}

export default connect(

mapStateToProps,

)(Menu)



Несвязанные компоненты, работающие с одними и теми же данными в состоянии

Бывают ситуации, когда нескольким, относительно несвязанным компонентам требуется доступ к одному и тому же состоянию.

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

Конечно, вы можете пойти на крайние меры и создать суперкомпонент верхнего уровня, который хранит данные профиля пользователя и сначала передает их компоненту заголовка и его дочерним компонентам, а затем передает их дальше вниз по дереву.

в компонент редактирования профиля.

В этом случае вам также потребуется передать обратный вызов форме редактирования профиля, которая будет вызываться при изменении данных пользователя.

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

Во-вторых, без дополнительных изменений кода вы, скорее всего, получите компоненты, которые сами не используют переданные в них данные, но будут рендериться каждый раз при обновлении данных, что снизит скорость работы приложения.

Можно сделать проще: сохраняем данные профиля пользователя в хранилище Redux, а компоненту-контейнеру заголовка и компоненту редактирования профиля разрешаем получать и изменять данные в магазине Redux.

Магазин Redux Против Состояния React

Плохой пример // App.js import React, {useState} from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import ProfileEditForm from '.



/elements/profileeditform';

const App = ({user}) => {

const [userName, setUserName] = useState(user.user_name);

return (

<div className="App">

<Header userName={userName} />

<main className="Main">

<ProfileEditForm onChangeName={setUserName} userName={userName} />

</main>

</div>

);

}

export default App;

// .

/elements/header.js import React from "react"; import logo from ".

/logo.svg"; import Menu from ".

/menu"; export default ({ userName }) => ( <header className="App-header"> <img src={logo} className="App-logo" alt="логотип" /> <Menu userName={userName} /> </header> ) // .



/elements/menu.js

import React from "react";

const Menu = ({userName}) => (

<>

<span>{userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

export default Menu;

// .



/elements/profileeditform.js

import React from "react";

export default ({userName, onChangeName}) => {

const handleChange = (e) => {

onChangeName(e.target.value);

};

return (

<form>

<input type="text" value={userName} onChange={handleChange} />

<button>{'Save'}</button>

</form>

)

}

Хороший пример // App.js import React from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import ProfileEditForm from '.

/elements/profileeditform'; const App = () => { return ( <div className="App"> <Header /> <main className="Main"> <ProfileEditForm /> </main> </div> ); } export default App; //.

/elements/header.js import React from "react"; import logo from ".

/logo.svg"; import Menu from ".

/menu"; export default () => ( <header className="App-header"> <img src={logo} className="App-logo" alt="логотип" /> <Menu /> </header> ) //.



/elements/menu.js

import React from "react";

import { connect } from "react-redux";

const Menu = ({userName}) => (

<>

<span>{userName}</span>

<nav>

<ul>

<li>Item 1</li>

<li>Item 2</li>

<li>Item 3</li>

<li>Item 4</li>

</ul>

</nav>

</>

)

const mapStateToProps = (state) => {

return {

userName: state.userInfo.userName,

}

}

export default connect(

mapStateToProps,

)(Menu)

//.

/elements/profileeditform import React from "react"; import { changeUserName } from '.



/actions/userAction'

import {connect} from "react-redux";

const ProfileEditForm = ({userName, changeUserName}) => {

const handleChange = (e) => {

changeUserName(e.target.value);

};

return (

<form>

<input type="text" value={userName} onChange={handleChange} />

<button>{'Save'}</button>

</form>

)

}

const mapStateToProps = (state) => {

return {

userName: state.userInfo.userName,

}

}

const mapDispatchToProps = { changeUserName }

export default connect(

mapStateToProps,

mapDispatchToProps,

)(ProfileEditForm)



Возможность отслеживать изменения состояния

Другой случай: вам нужно реализовать в приложении возможность отмены/повтора пользовательских действий или вы просто хотите протоколировать изменения состояния.

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

В таких случаях Redux — отличное решение, потому что… Каждое создаваемое действие — это атомарное изменение состояния.

Redux упрощает все эти задачи, концентрируя их в одном месте — магазине Redux. Пример отмены/повтора // App.js import React from 'react'; import '.

/App.css'; import Header from '.

/elements/header'; import ProfileEditForm from '.

/elements/profileeditform'; const App = () => { return ( <div className="App"> <Header /> <main className="Main"> <ProfileEditForm /> </main> </div> ); } export default App; // '.

/elements/profileeditform.js' import React from "react"; import { changeUserName, undo, redo } from '.



/actions/userAction'

import {connect} from "react-redux";

const ProfileEditForm = ({ userName, changeUserName, undo, redo, hasPast, hasFuture }) => {

const handleChange = (e) => {

changeUserName(e.target.value);

};

return (

<>

<form>

<input type="text" value={userName} onChange={handleChange} />

<button>{'Save'}</button>

</form>

<div>

<button onClick={undo} disabled={!hasPast}>{'Undo'}</button>

<button onClick={redo} disabled={!hasFuture}>{'Redo'}</button>

</div>

</>

)

}

const mapStateToProps = (state) => {

return {

hasPast: !!state.userInfo.past.length,

hasFuture: !!state.userInfo.future.length,

userName: state.userInfo.present.userName,

}

}

const mapDispatchToProps = { changeUserName, undo, redo }

export default connect(

mapStateToProps,

mapDispatchToProps,

)(ProfileEditForm)

// src/constants/userConstants.js

export const userConstants = {

SET_USER_NAME: 'USER_SET_USER_NAME',

UNDO: 'USER_UNDO',

REDO: 'USER_REDO',

};

// src/actions/userAction.js

import { userConstants } from ".



/constants/userConstants";

export const changeUserName = (userName) => (

{

type: userConstants.SET_USER_NAME,

payload: userName,

}

);

export const undo = () => (

{

type: userConstants.UNDO,

}

);

export const redo = () => (

{

type: userConstants.REDO,

}

);

// src/reducers/user/undoableUserReducer.js

import {userConstants} from ".

/.



/constants/userConstants";

export function undoable(reducer) {

const initialState = {

past: [],

present: reducer(undefined, {}),

future: [],

};

return function userReducer(state = initialState, action) {

const {past, present, future} = state;

switch (action.type) {

case userConstants.UNDO:

const previous = past[past.length - 1]

const newPast = past.slice(0, past.length - 1)

return {

past: newPast,

present: previous,

future: [present, .

future] } case userConstants.REDO: const next = future[0] const newFuture = future.slice(1) return { past: [.

past, present], present: next, future: newFuture } default: const newPresent = reducer(present, action) if (present === newPresent) { return state } return { past: [.

past, present], present: newPresent, future: [] } } } } // src/reducers/user/userReducer.js import { undoable } from ".

/undoableUserReducer"; import { userConstants } from ".

/.

/constants/userConstants"; const initialState = { userName: 'username', }; function reducer(state = initialState, action) { switch (action.type) { case userConstants.SET_USER_NAME: return { .

state, userName: action.payload, }; default: return state; } } export const userReducer = undoable(reducer);



Обобщить

Вам следует рассмотреть возможность хранения данных в хранилище Redux в следующих случаях:
  1. Если эти данные меняются редко;
  2. Если одни и те же данные используются в нескольких (более 2-3) связанных компонентах или в несвязанных компонентах;
  3. Если вам нужно отслеживать изменения данных.

Во всех остальных случаях лучше использовать состояние React. P.S. Большое спасибо мамдакс111 за помощь в подготовке статьи! Теги: #JavaScript #frontend #react.js #node.js #redux
Вместе с данным постом часто просматривают: