Redux store vs React state

كيف تصمم تخزين البيانات في تطبيق React؟ أين يتم تخزين بيانات التطبيق: في التخزين العالمي (متجر Redux) أو في التخزين المحلي (حالة المكون)؟

تنشأ مثل هذه الأسئلة من المطورين الذين بدأوا في استخدام مكتبة Redux ، وحتى أولئك الذين يستخدمونها بنشاط.



لمدة 5 سنوات من التطوير على React ، اختبرنا في BENOVATE مناهج مختلفة لبناء بنية هذه التطبيقات في الممارسة. في هذه المقالة ، سننظر في المعايير المحتملة لاختيار مكان تخزين البيانات في تطبيق.



ربما بدون Redux على الإطلاق؟ نعم ، إذا كنت تستطيع الاستغناء عنه. حول هذا الموضوع ، يمكنك قراءة مقال من أحد منشئي المكتبة - دان أبراموف. إذا كان المطور يفهم أن Redux لا غنى عنه ، فهناك العديد من المعايير لاختيار مستودع البيانات:



  1. عمر البيانات
  2. تردد الاستخدام
  3. القدرة على تتبع التغيرات في الحالة


عمر البيانات



هناك فئتان:



  • البيانات المتغيرة باستمرار.
  • نادرا ما تتغير البيانات. نادرًا ما تتغير هذه البيانات أثناء تفاعل المستخدم المباشر مع التطبيق أو بين جلسات التطبيق.


تغيير البيانات بشكل متكرر



تتضمن هذه الفئة ، على سبيل المثال ، معلمات التصفية والفرز والتنقل صفحة تلو الأخرى لمكون يقوم بتنفيذ العمل مع قائمة من الكائنات ، أو علامة مسؤولة عن عرض عناصر واجهة المستخدم الفردية في تطبيق ، على سبيل المثال ، قائمة منسدلة أو نافذة مشروطة (شريطة ألا تكون مرتبطة. إلى إعدادات المستخدم). يمكن أن يتضمن هذا أيضًا بيانات النموذج الذي يتم ملؤه ، حتى يتم إرسالها إلى الخادم.



من الأفضل تخزين هذه البيانات في حالة المكون ، لأنه إنهم يفسدون التخزين العالمي ويعقدون العمل معهم: تحتاج إلى كتابة الإجراءات ، والمخفضات ، وتهيئة الحالة ومسحها في الوقت المناسب.



مثالا سيئا
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;
    }
}




تردد الاستخدام



المعيار الثاني هو عدد المكونات في تطبيق React التي يجب أن يكون لها حق الوصول إلى نفس الحالة. كلما زادت المكونات التي تستخدم نفس البيانات في الحالة ، زادت الفائدة التي تحصل عليها من استخدام متجر Redux.



إذا كنت تفهم أنه بالنسبة لمكون معين أو جزء صغير من التطبيق الخاص بك ، يتم عزل الحالة ، فمن الأفضل استخدام حالة رد الفعل لمكون منفصل أو مكون HOC.



عمق نقل الدولة



في التطبيقات غير Redux ، يجب تخزين بيانات حالة التفاعل في المكون الأعلى (في الشجرة) الذي تحتاج مكوناته الفرعية إلى الوصول إلى هذه البيانات ، على افتراض أننا نتجنب تخزين نفس البيانات في أماكن مختلفة.



في بعض الأحيان ، يتطلب عدد كبير من المكونات الفرعية بمستويات تداخل مختلفة البيانات من حالة المكون الرئيسي ، مما يؤدي إلى تشابك قوي بين المكونات وظهور رمز غير ذي فائدة فيها ، وهو أمر مكلف للتعديل في كل مرة تجد فيها أن المكون الفرعي يحتاج إلى الوصول إلى بيانات الحالة الجديدة. في مثل هذه الحالات ، من الحكمة حفظ الحالة في Redux واسترداد البيانات اللازمة من التخزين في المكونات المقابلة.



إذا كان من الضروري نقل بيانات الحالة إلى مكونات تابعة على مستوى واحد أو مستويين من التداخل ، فيمكنك القيام بذلك بدون 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)




المكونات غير ذات الصلة التي تعمل على نفس البيانات في الحالة



هناك حالات يحتاج فيها العديد من المكونات غير ذات الصلة نسبيًا إلى الوصول إلى نفس الحالة. على سبيل المثال ، يحتاج التطبيق إلى إنشاء نموذج لتحرير ملف تعريف المستخدم ورأس ، والذي يحتاج أيضًا إلى عرض بيانات المستخدم.



بالطبع ، يمكنك الوصول إلى أقصى حد عندما تقوم بإنشاء مكون فائق عالي المستوى يقوم بتخزين بيانات ملف تعريف المستخدم ، أولاً ، يمررها إلى مكون الرأس وأطفاله ، وثانيًا ، يمررها إلى أعماق الشجرة. إلى مكون تحرير ملف التعريف. في هذه الحالة ، ستحتاج أيضًا إلى تحويل رد الاتصال إلى نموذج تحرير الملف الشخصي ، والذي سيتم استدعاؤه عند تغير بيانات المستخدم.



أولاً ، من المحتمل أن يؤدي هذا النهج إلى ارتباط قوي بين المكونات ، وظهور البيانات غير الضرورية والرمز غير الضروري في المكونات الوسيطة ، الأمر الذي سيستغرق وقتًا للتحديث والصيانة.



ثانيًا ، بدون تغييرات رمز إضافية ، على الأرجح ستحصل على مكونات لا تستخدم نفسها البيانات التي تم تمريرها إليها ، ولكن سيتم تقديمها في كل مرة يتم فيها تحديث هذه البيانات ، مما سيؤدي إلى انخفاض في سرعة التطبيق.



لتسهيل الأمر ، نقوم بحفظ بيانات ملف تعريف المستخدم في متجر 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 كل هذه المهام من خلال الاحتفاظ بها في مكان واحد - متجر 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. إذا كنت ترغب في تتبع تغييرات البيانات.


في جميع الحالات الأخرى ، من الأفضل استخدام حالة رد الفعل.



PS شكرا جزيلامامداكس للمساعدة في إعداد المقال!



All Articles