Redux-Speicher vs Reaktionsstatus

Wie gestalte ich die Datenspeicherung in einer React-Anwendung? Wo werden Anwendungsdaten gespeichert: im globalen Speicher (Redux-Speicher) oder im lokalen Speicher (Komponentenstatus)?

Solche Fragen stellen sich Entwicklern, die mit der Nutzung der Redux-Bibliothek beginnen, und sogar solchen, die sie aktiv nutzen.



Während der 5-jährigen Entwicklung von React haben wir bei BENOVATE verschiedene Ansätze zum Aufbau der Architektur solcher Anwendungen in der Praxis getestet. In diesem Artikel werden mögliche Kriterien für die Auswahl des Speicherorts von Daten in einer Anwendung berücksichtigt.



Oder vielleicht ohne Redux? Ja, wenn Sie darauf verzichten können. Sie können einen Artikel von einem der Schöpfer der Bibliothek, Dan Abramov, zu diesem Thema lesen . Wenn der Entwickler versteht, dass Redux unverzichtbar ist, können mehrere Kriterien für die Auswahl eines Data Warehouse identifiziert werden:



  1. Datenlebensdauer
  2. Häufigkeit der Nutzung
  3. Fähigkeit, Zustandsänderungen zu verfolgen


Datenlebensdauer



Es gibt 2 Kategorien:



  • Häufig wechselnde Daten.
  • Daten werden selten geändert. Solche Daten ändern sich selten während der direkten Benutzerinteraktion mit der Anwendung oder zwischen Anwendungssitzungen.


Häufig wechselnde Daten



Diese Kategorie umfasst beispielsweise das Filtern, Sortieren und Paging der Navigation einer Komponente, die eine Liste von Objekten implementiert, oder ein Flag, das für die Anzeige einzelner UI-Elemente in einer Anwendung verantwortlich ist, z. B. eine Dropdown-Liste oder ein modales Fenster (sofern es nicht gebunden ist) zu Benutzereinstellungen). Dies schließt auch die Daten des ausgefüllten Formulars ein, bis sie an den Server gesendet werden.



Solche Daten werden am besten im Zustand der Komponente gespeichert, weil Sie überladen den globalen Speicher und erschweren die Arbeit mit ihnen: Sie müssen Aktionen und Reduzierungen schreiben, den Status initialisieren und rechtzeitig löschen.



Schlechtes Beispiel
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;
    }
}




Gutes Beispiel
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;




Daten werden selten geändert



Dies sind Daten, die sich normalerweise nicht zwischen Seitenaktualisierungen oder zwischen einzelnen Besuchen einer Seite durch einen Benutzer ändern.



Da der Redux-Speicher beim Aktualisieren der Seite neu erstellt wird, muss dieser Datentyp an einem anderen Ort gespeichert werden: in einer Datenbank auf dem Server oder in einem lokalen Speicher in einem Browser.



Dies können Daten aus Verzeichnissen oder benutzerdefinierten Einstellungen sein. Wenn Sie beispielsweise eine Anwendung entwickeln, die Benutzereinstellungen verwendet, speichern wir diese Einstellungen nach der Benutzerauthentifizierung im Redux-Speicher, sodass die Anwendungskomponenten ohne Zugriff auf den Server verwendet werden können.



Beachten Sie, dass sich einige Daten auf dem Server ohne Benutzereingriff ändern können und Sie vorhersehen müssen, wie Ihre Anwendung darauf reagieren wird.



Schlechtes Beispiel
// 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;




Gutes Beispiel
// 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;
    }
}




Häufigkeit der Nutzung



Das zweite Kriterium ist, wie viele Komponenten in einer React-Anwendung Zugriff auf denselben Status haben sollen. Je mehr Komponenten dieselben Daten im Status verwenden, desto größer ist der Nutzen der Verwendung des Redux-Speichers.



Wenn Sie verstehen, dass für eine bestimmte Komponente oder einen kleinen Teil Ihrer Anwendung der Status isoliert ist, ist es besser, den Reaktionsstatus einer separaten Komponente oder HOC-Komponente zu verwenden.



Zustandstransfertiefe



In Anwendungen ohne Redux sollten Reaktionsstatusdaten in der obersten (im Baum) Komponente gespeichert werden, deren untergeordnete Komponenten Zugriff auf diese Daten benötigen, unter der Annahme, dass wir vermeiden, dieselben Daten an verschiedenen Orten zu speichern.



Manchmal werden Daten aus dem Status der übergeordneten Komponente von einer großen Anzahl untergeordneter Komponenten auf verschiedenen Verschachtelungsebenen benötigt, was zu einer starken Verknüpfung der Komponenten und dem Auftreten von nutzlosem Code in ihnen führt. Die Bearbeitung ist teuer, wenn Sie feststellen, dass die untergeordnete Komponente Zugriff auf neue Statusdaten benötigt. In solchen Fällen ist es sinnvoller, den Status in Redux zu speichern und die gewünschten Daten aus dem Speicher in den entsprechenden Komponenten abzurufen.



Wenn Sie Statusdaten an untergeordnete Komponenten mit einer oder zwei Verschachtelungsebenen übergeben müssen, können Sie dies ohne Redux tun.



Schlechtes Beispiel
//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>
    </>
)




Gutes Beispiel
// 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)




Ungebundene Komponenten, die im Status mit denselben Daten arbeiten



Es gibt Situationen, in denen mehrere, relativ unabhängige Komponenten Zugriff auf denselben Status benötigen. In der Anwendung müssen Sie beispielsweise ein Formular zum Bearbeiten des Benutzerprofils und der Kopfzeile erstellen, in dem Sie auch Benutzerdaten anzeigen möchten.



Natürlich können Sie bis zum Äußersten gehen, wenn Sie eine Superkomponente der obersten Ebene erstellen, die Benutzerprofildaten speichert und diese erstens an die Headerkomponente und ihre untergeordneten Komponenten und zweitens tiefer im Baum weitergibt. zur Profilbearbeitungskomponente. In diesem Fall müssen Sie auch einen Rückruf an das Profilbearbeitungsformular übertragen, das aufgerufen wird, wenn sich die Benutzerdaten ändern.



Erstens führt dieser Ansatz wahrscheinlich zu einer starken Verzahnung von Komponenten, dem Auftreten unnötiger Daten und unnötigen Codes in Zwischenkomponenten, deren Aktualisierung und Wartung einige Zeit in Anspruch nimmt.



Zweitens erhalten Sie ohne zusätzliche Codeänderungen höchstwahrscheinlich Komponenten, die die an sie übergebenen Daten nicht selbst verwenden, sondern jedes Mal gerendert werden, wenn diese Daten aktualisiert werden, was zu einer Verringerung der Geschwindigkeit der Anwendung führt.



Zur Vereinfachung speichern wir die Profildaten des Benutzers im Redux-Speicher und lassen die Header-Container-Komponente und die Profilbearbeitungskomponente die Daten im Redux-Speicher empfangen und ändern.



Bild



Schlechtes Beispiel
// 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>
    )
}




Gutes Beispiel
// 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)




Fähigkeit, Zustandsänderungen zu verfolgen



Ein weiterer Fall: Sie müssen die Möglichkeit implementieren, Benutzervorgänge in der Anwendung rückgängig zu machen / zu wiederholen, oder Sie möchten nur Statusänderungen protokollieren.



Ein solcher Bedarf entstand während der Entwicklung des Tutorial-Designers, mit dem der Benutzer Blöcke mit Text, Bild und Video auf der Handbuchseite hinzufügen und konfigurieren sowie Rückgängig / Wiederherstellen-Vorgänge ausführen kann.



In diesen Fällen ist Redux eine großartige Lösung, weil Jede erzeugte Aktion ist eine atomare Zustandsänderung. Redux vereinfacht all diese Aufgaben, indem es sie an einem Ort konzentriert - im Redux-Store.



Beispiel rückgängig machen / wiederholen
// 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);




Zusammenfassen



In den folgenden Fällen sollten Sie Daten im Redux-Speicher speichern:



  1. Wenn sich diese Daten selten ändern;
  2. Wenn dieselben Daten in mehreren (mehr als 2-3) verbundenen Komponenten oder in nicht verwandten Komponenten verwendet werden;
  3. Wenn Sie Datenänderungen verfolgen möchten.


In allen anderen Fällen ist es besser, den Status "Reagieren" zu verwenden.



PS Vielen Dankmamdaxx111 für Hilfe bei der Vorbereitung des Artikels!



All Articles