How to design data storage in a React application? Where to store application data: in the global storage (Redux store) or in local storage (component state)?
Such questions arise for developers beginning to use the Redux library, and even for those who actively use it.

Over 5 years of development at React, we at BENOVATE have tested in practice various approaches to building the architecture of such applications. In this article, we will consider the possible criteria for choosing the location of data storage in the application.

Or maybe without Redux? Yes, if you can do without it. You can read the article from one of the creators of the library on this subject - Dan Abramov. If the developer understands that Redux is indispensable, then there are several criteria for choosing a data warehouse:

  1. Data Life Span
  2. Frequency of use
  3. Ability to track changes in state

Data Lifetime


There are 2 categories:

  • Often changing data.
  • Rarely 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, filtering, sorting and paging navigation of a component that implements a list of objects, or a flag responsible for displaying individual UI elements in an application, for example, a drop-down list or a modal window (provided that it is not bound to user settings). This also includes the data of the filled form until they are sent to the server.

Such data is best stored in the state of the component, because they clutter up the global storage and complicate the work with them: you need to write actions, reducers, initialize the state and clean 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; 


Rarely 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 repository is re-created when the page is refreshed, this type of data should be stored somewhere else: in the database on the server or in the local repository in the browser.

It can be the data of directories or user settings. For example, when developing an application that uses user settings, after user authentication, we save these settings in the Redux store, which allows the application components to use them without accessing 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 that use the same data in state, the greater the benefit of using the Redux store.

If you understand that for a specific component or small part of your application, state is isolated, then it is better to use the React state of a separate component or HOC component.

Depth of state transfer


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 data from the state of the parent component is required by a large number of child components at different levels of nesting, which leads to strong linkage of the 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 at one or two levels of nesting, 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, in the application you need to create a form for editing the user profile and header, in which you also need 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, you will also need to send a callback to the profile editing form, which will be called when user data is changed.

Firstly, this approach is likely to lead to a strong linkage of components, the appearance of unnecessary data and unnecessary code in the intermediate components, which will take time to update and support.

Secondly, without additional code changes, most likely you will get components that themselves do not use the data transferred to them, but will be rendered every time this data is updated, which will lead to a decrease in the application’s speed.

You can make it simpler: save the user profile data in the Redux store, and allow the header container component and the profile editing component to receive and modify data in the Redux store.

image

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 realize the ability to undo/redo user operations in the application or you just want to log state changes.

Such a need arose during the development of the tutorial designer, with which the user can add and customize blocks with text, image and video on the manual page, and can also perform Undo/Redo operations.

In such cases, Redux is a great solution because every action created is an atomic state change. 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); 


Summarizing


Consider the option of storing data in the Redux store in the following cases:

  1. If this data rarely changes;
  2. If the same data is used in several (more than 2-3) related components or in unrelated components;
  3. If you want to track data changes.

In all other cases, it is better to use React state.

P.S. Thanks a lot mamdaxx111 for your help in preparing this article !.

Source