ReduxストアとReact状態

Reactアプリケーションでデータストレージを設計するにはどうすればよいですか?アプリケーションデータを保存する場所:グローバルストレージ(Reduxストア)またはローカルストレージ(コンポーネントの状態)

このような疑問は、Reduxライブラリの使用を開始する開発者や、積極的に使用する開発者にとっても発生します。



Reactでの5年間の開発で、BENOVATEは、そのようなアプリケーションのアーキテクチャを構築するためのさまざまなアプローチを実際にテストしました。この記事では、アプリケーションのどこにデータを格納するかを選択するための考えられる基準を検討します。



それともReduxなし?はい、それなしでできるなら。この問題については、ライブラリの作成者の1人であるDan Abramovの記事を読むことができますReduxが不可欠であることを開発者が理解している場合、データウェアハウスを選択するためのいくつかの基準があります。



  1. データの寿命
  2. 使用頻度
  3. 状態の変化を追跡する機能


データの寿命



2つのカテゴリがあります。



  • 頻繁に変更されるデータ。
  • まれに変更されるデータ。このようなデータは、ユーザーがアプリケーションを使用して直接作業している間、またはアプリケーションとのセッション間で変更されることはほとんどありません。


頻繁に変化するデータ



このカテゴリには、たとえば、オブジェクトのリストを操作するコンポーネントのフィルタリング、並べ替え、およびページングオプション、またはアプリケーションに個々のUI要素を表示するフラグ(ドロップダウンリストやモーダルウィンドウなど)が含まれます(バインドされていない場合)。ユーザー設定に)。これには、サーバーに送信されるまで、入力中のフォームのデータを含めることもできます。



そのようなデータをコンポーネントの状態で保存する方が良いです。それらはグローバルストレージを混乱させ、それらとの作業を複雑にします。アクション、レデューサーを記述し、状態を初期化し、時間内にクリアする必要があります。



悪い例
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;
    }
}




良い例え
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;




まれに変化するデータ



これは通常、ページの更新間またはユーザーによるページへの個別の訪問間で変化しないデータです。



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="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;




良い例え
// 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;
    }
}




使用頻度



2番目の基準は、Reactアプリケーションのコンポーネントのうち、同じ状態にアクセスできるコンポーネントの数です。状態が同じデータを使用するコンポーネントが多いほど、Reduxストアを使用することで得られるメリットが大きくなります。



状態が特定のコンポーネントまたはアプリケーションのごく一部に分離されていることを理解している場合は、別のコンポーネントまたはHOCコンポーネントのReact状態を使用することをお勧めします。



状態の深さ



Redux以外のアプリケーションでは、同じデータを別の場所に格納しないことを前提として、React状態データは、子コンポーネントがこのデータにアクセスする必要がある最上位(ツリー内)コンポーネントに格納する必要があります。



場合によっては、親コンポーネントの状態からのデータが、さまざまな入れ子レベルの多数の子コンポーネントで必要となるため、コンポーネントが強力に連動し、それらに不要なコードが表示されることがあります。このような場合、Reduxに状態を保存し、適切なコンポーネントのストレージから目的のデータを取得する方が理にかなっています。



状態データを1つまたは2つのネストレベルの子コンポーネントに渡す必要がある場合は、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="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>
    </>
)




良い例え
// 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)




状態が同じデータを操作する非バインドコンポーネント



比較的関連のないいくつかのコンポーネントが同じ状態にアクセスする必要がある状況があります。たとえば、アプリケーションは、ユーザープロファイルとヘッダーを編集するためのフォームを作成する必要があり、ヘッダーもユーザーデータを表示する必要があります。



もちろん、ユーザープロファイルデータを格納する最上位のスーパーコンポーネントを作成し、最初にそれをヘッダーコンポーネントとその子に渡し、次に、ツリーのより深いところに渡すことで、極端に進むことができます。プロファイル編集コンポーネントに。同時に、ユーザーデータが変更されたときに呼び出されるプロファイル編集フォームにもコールバックが必要です。



まず、このアプローチは、コンポーネントの強力な連動、中間コンポーネントでの不要なデータと不要なコードの出現につながる可能性が高く、更新と保守に時間がかかります。



第2に、追加のコードを変更しないと、渡されたデータを使用しないコンポーネントを取得する可能性が高くなりますが、このデータが更新されるたびにレンダリングされるため、アプリケーションの速度が低下します。



簡単にするために、ユーザーのプロファイルデータをReduxストアに保存し、ヘッダーコンテナーコンポーネントとプロファイル編集コンポーネントがReduxストアのデータを受信および変更できるようにします。



画像



悪い例
// 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>
    )
}




良い例え
// 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)




状態の変化を追跡する機能



別のケース:アプリケーションでユーザー操作を元に戻す/やり直す機能を実装する必要がある場合、または単に状態の変化をログに記録したい場合。



このようなニーズは、チュートリアルデザイナーの開発中に発生しました。ユーザーは、マニュアルページでテキスト、画像、ビデオを使用してブロックを追加および構成したり、元に戻す/やり直し操作を実行したりできます。



これらの場合、Reduxは優れたソリューションです。作成されるすべてのアクションは、状態へのアトミックな変更です。Reduxは、これらのタスクを1つの場所(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状態を使用することをお勧めします。



PSどうもありがとうmamdaxx111 記事の準備を手伝ってください!



All Articles