Tienda de Redux vs estado de reacción

¿Cómo diseño el almacenamiento de datos en una aplicación React? ¿Dónde almacenar los datos de la aplicación: en la tienda global (tienda Redux) o en la tienda local (estado del componente)?

Estas preguntas surgen de los desarrolladores que están comenzando a usar la biblioteca Redux, e incluso de aquellos que la usan activamente.



Durante 5 años de desarrollo en React, en BENOVATE hemos probado varios enfoques para construir la arquitectura de tales aplicaciones en la práctica. En este artículo, consideraremos los posibles criterios para elegir dónde almacenar los datos en una aplicación.



¿Quizás sin Redux en absoluto? Sí, si puedes prescindir de él. Puede leer un artículo de uno de los creadores de la biblioteca, Dan Abramov, sobre este tema . Si el desarrollador entiende que Redux es indispensable, existen varios criterios para elegir un almacén de datos:



  1. Vida de datos
  2. Frecuencia de uso
  3. Capacidad para rastrear cambios en el estado


Vida de datos



Hay 2 categorías:



  • Datos que cambian con frecuencia.
  • Datos que cambian raramente. Dichos datos rara vez cambian durante el trabajo directo del usuario con la aplicación o entre sesiones con la aplicación.


Datos que cambian con frecuencia



Esta categoría incluye, por ejemplo, los parámetros de filtrado, clasificación y navegación página por página de un componente que implementa el trabajo con una lista de objetos, o un indicador que es responsable de mostrar elementos de IU individuales en una aplicación, por ejemplo, una lista desplegable o una ventana modal (siempre que no esté anclada a la configuración del usuario). Esto también puede incluir los datos del formulario que se está completando, hasta que se envían al servidor.



Es mejor almacenar dichos datos en el estado del componente, ya que ellos abarrotan el almacenamiento global y complican el trabajo con ellos: necesita escribir acciones, reductores, inicializar el estado y borrarlo a tiempo.



Mal ejemplo
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;
    }
}




Buen ejemplo
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;




Datos que cambian raramente



Estos son datos que generalmente no cambian entre actualizaciones de página o entre visitas individuales a una página por parte de un usuario.



Dado que la tienda Redux se vuelve a crear cuando se actualiza la página, este tipo de datos debe almacenarse en otro lugar: en una base de datos en el servidor o en una tienda local en un navegador.



Pueden ser los datos de directorios o configuraciones de usuario. Por ejemplo, al desarrollar una aplicación que usa configuraciones personalizadas, después de la autenticación del usuario, guardamos estas configuraciones en la tienda Redux, que permite que los componentes de la aplicación las usen sin ir al servidor.



Vale la pena recordar que algunos datos pueden cambiar en el servidor sin la intervención del usuario, y debe considerar cómo responderá su aplicación.



Mal ejemplo
// 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;




Buen ejemplo
// 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;
    }
}




Frecuencia de uso



El segundo criterio es cuántos componentes en una aplicación React deberían tener acceso al mismo estado. Cuantos más componentes usen los mismos datos en estado, mayor será el beneficio de usar la tienda Redux.



Si comprende que el estado está aislado para un componente específico o una pequeña parte de su aplicación, entonces es mejor usar el estado Reaccionar de un componente separado o componente HOC.



Profundidad del estado



En las aplicaciones que no son de Redux, los datos de estado React deben almacenarse en el componente superior (en el árbol) cuyos componentes secundarios necesitan acceder a estos datos, en el supuesto de que evitemos almacenar los mismos datos en diferentes lugares.



A veces, una gran cantidad de componentes secundarios requieren datos del estado de un componente primario en diferentes niveles de anidamiento, lo que conduce a un fuerte enclavamiento de componentes y a la aparición de código inútil en ellos, lo cual es costoso de editar cada vez que encuentra que el componente secundario necesita acceso a nuevos datos de estado. En tales casos, es más sabio guardar el estado en Redux y recuperar los datos necesarios del almacenamiento en los componentes correspondientes.



Si es necesario transferir datos de estado a componentes secundarios en uno o dos niveles de anidamiento, puede hacerlo sin Redux.



Mal ejemplo
//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>
    </>
)




Buen ejemplo
// 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)




Componentes independientes que operan en los mismos datos en estado



Hay situaciones en las que varios componentes relativamente no relacionados necesitan acceder al mismo estado. Por ejemplo, una aplicación necesita crear un formulario para editar un perfil de usuario y un encabezado, que también necesita mostrar los datos del usuario.



Por supuesto, puede llegar al extremo cuando crea un supercomponente de nivel superior que almacena datos de perfil de usuario y, primero, lo pasa al componente de encabezado y sus elementos secundarios, y segundo, lo pasa más abajo en el árbol. al componente de edición de perfil. En este caso, también deberá transferir una devolución de llamada al formulario de edición de perfil, que se llamará cuando cambien los datos del usuario.



En primer lugar, es probable que este enfoque conduzca a una fuerte vinculación de componentes, la aparición de datos innecesarios y código innecesario en los componentes intermedios, lo que llevará tiempo actualizar y mantener.



En segundo lugar, sin cambios de código adicionales, lo más probable es que obtenga componentes que no utilizan los datos que se les transfieren, sino que se mostrarán cada vez que se actualicen estos datos, lo que reducirá la velocidad de la aplicación.



Para hacerlo más fácil, guardamos los datos del perfil del usuario en la tienda Redux y dejamos que el componente contenedor de encabezado y el componente de edición de perfil reciban y modifiquen los datos en la tienda Redux.



imagen



Mal ejemplo
// 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>
    )
}




Buen ejemplo
// 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)




Capacidad para rastrear cambios en el estado



Otro caso: debe darse cuenta de la capacidad de deshacer / rehacer las operaciones del usuario en la aplicación o simplemente desea registrar los cambios de estado.



Tuvimos tanta necesidad al desarrollar un diseñador de tutoriales, con el cual el usuario puede agregar y personalizar bloques con texto, imágenes y video en la página del tutorial, y también puede realizar operaciones Deshacer / Rehacer.



En tales casos, Redux es una gran solución porque Cada acción creada es un cambio atómico al estado. Redux simplifica todas estas tareas al mantenerlas en un solo lugar: la tienda Redux.



Ejemplo de deshacer / rehacer
// 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);




Resumiendo



Considere la opción de almacenar datos en la tienda Redux en los siguientes casos:



  1. Si estos datos rara vez cambian;
  2. Si se utilizan los mismos datos en varios (más de 2-3) componentes relacionados o en componentes no relacionados;
  3. Si desea realizar un seguimiento de los cambios de datos.


En todos los demás casos, es mejor usar el estado Reaccionar.



PD Muchas graciasmamdaxx111 por ayuda en la preparación del artículo!



All Articles