Such questions arise from developers who are starting to use the Redux library, and even those who actively use it.
For 5 years of development on React, we at BENOVATE have tested various approaches to building the architecture of such applications in practice. In the article, we consider the possible criteria for choosing the location of data storage in the application.
Maybe without Redux at all? Yes, if you can do without it. You can read an article from one of the creators of the library, Dan Abramov, on this subject . If the developer understands that Redux is indispensable, then there are several criteria for choosing a data warehouse:
- Data lifespan
- Frequency of use
- Ability to track changes in state
Data lifespan
There are 2 categories:
- Frequently changing data.
- Infrequently changing data. Such data rarely changes during the user's direct work with the application or between sessions with the application.
Frequently changing data
This category includes, for example, the filtering, sorting and page-by-page navigation parameters of a component that implements work with a list of objects, or a flag that is responsible for displaying individual UI elements in an application, for example, a drop-down list or a modal window (provided that it is not anchored to user settings). This also includes the data of the filled form until they are sent to the server.
It is better to store such data in the state of the component, since they clutter up the global storage and complicate work with them: you need to write actions, reducers, initialize the state and clear it in time.
Bad example
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="logo" />
</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;
}
}
Good example
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="logo" />
</header>
<main className="Main">
<button onClick={() => setOpenModal(true)}>{'Open Modal'}</button>
</main>
<Modal isOpen={openModal} onClose={() => setOpenModal(false)} />
</div>
);
}
export default App;
Infrequently changing data
This is data that usually does not change between page updates or between individual visits to a page by a user.
Since the Redux store is re-created when the page is refreshed, this type of data must be stored somewhere else: in a database on the server or in a local store in a browser.
It can be the data of directories or user settings. For example, when developing an application that uses custom settings, after user authentication, we save these settings in the Redux store, which allows application components to use them without going to the server.
It is worth remembering that some data may change on the server without user intervention, and you must consider how your application will respond to it.
Bad example
// 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="logo" />
<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;
Good example
// 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="logo" />
<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;
}
}
Frequency of use
The second criterion is how many components in a React application should have access to the same state. The more components use the same data in state, the more benefit you get from using the Redux store.
If you understand that state is isolated for a specific component or a small part of your application, then it is better to use the React state of a separate component or HOC component.
State transfer depth
In applications without Redux, React state data should be stored in the topmost (in the tree) component, whose child components will need access to this data, on the assumption that we avoid storing the same data in different places.
Sometimes the data from the state of the parent component is required by a large number of child components at different nesting levels, which leads to strong interlocking of components and the appearance of useless code in them, which is expensive to edit every time you find that the child component needs access to new state data. In such cases, it is more reasonable to save state in Redux and retrieve the necessary data from the storage in the corresponding components.
If you need to pass state data to child components one or two levels of nesting, then you can do this without Redux.
Bad example
//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="logo" />
<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>
</>
)
Good example
// 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="logo" />
<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)
Unbound components that operate on the same data in state
There are situations when several, relatively unrelated components need access to the same state. For example, an application needs to create a form for editing a user profile and a header, which also needs to display user data.
Of course, you can go to extremes when you create a top-level super-component that stores user profile data and, firstly, transfers them to the header component and its child components, and secondly, transfers them deeper into the tree, to the profile editing component. At the same time, a callback will also be required in the profile editing form, which will be called when user data is changed.
First, this approach is likely to lead to strong interlocking of components, the appearance of unnecessary data and unnecessary code in intermediate components, which will take time to update and maintain.
Secondly, without additional code changes, most likely you will get components that do not themselves use the data passed to them, but will be rendered every time this data is updated, which will lead to a decrease in the speed of the application.
To make it easier, we save the user's profile data in the Redux store, and let the header container component and the profile editing component receive and modify the data in the Redux store.
Bad example
// 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="logo" />
<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>
)
}
Good example
// 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="logo" />
<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)
Ability to track changes in state
Another case: you need to implement the ability to undo / redo user operations in the application, or you just want to log state changes.
We had such a need when developing a tutorial designer, with which the user can add and customize blocks with text, images and video on the tutorial page, and can also perform Undo / Redo operations.
In such cases, Redux is a great solution because every action created is an atomic change to state. Redux simplifies all these tasks by focusing them in one place - Redux store.
Undo / redo example
// 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);
To summarize
Consider the option of storing data in the Redux store in the following cases:
- If this data rarely changes;
- If the same data is used in several (more than 2-3) related components or in unrelated components;
- If you want to track data changes.
In all other cases, it is better to use React state.
PS thank you very muchmamdaxx111 for help in preparing the article!