Redux store vs React state

Bagaimana cara mendesain penyimpanan data dalam aplikasi Bereaksi? Di mana menyimpan data aplikasi: di toko global (toko Redux) atau di toko lokal (keadaan komponen)?

Pertanyaan semacam itu muncul dari pengembang yang mulai menggunakan pustaka Redux, dan bahkan mereka yang secara aktif menggunakannya.



Selama 5 tahun pengembangan di React, kami di BENOVATE telah menguji berbagai pendekatan untuk membangun arsitektur aplikasi tersebut dalam praktiknya. Pada artikel ini, kami akan mempertimbangkan kriteria yang memungkinkan untuk memilih tempat menyimpan data dalam suatu aplikasi.



Atau mungkin tanpa Redux? Ya, jika Anda bisa melakukannya tanpa itu. Pada topik ini, Anda dapat membaca artikel dari salah satu pencipta perpustakaan - Dan Abramov. Jika pengembang memahami bahwa Redux sangat diperlukan, maka ada beberapa kriteria untuk memilih data warehouse:



  1. Umur data
  2. Frekuensi penggunaan
  3. Kemampuan untuk melacak perubahan dalam status


Umur data



Ada 2 kategori:



  • Sering mengubah data.
  • Jarang mengubah data. Data tersebut jarang berubah selama pekerjaan langsung pengguna dengan aplikasi atau antara sesi dengan aplikasi.


Sering Mengubah Data



Kategori ini mencakup, misalnya, parameter pemfilteran, pengurutan dan navigasi halaman-per-komponen dari komponen yang mengimplementasikan pekerjaan dengan daftar objek, atau flag yang bertanggung jawab untuk menampilkan elemen UI individu dalam suatu aplikasi, misalnya, daftar drop-down atau jendela modal (asalkan itu tidak berlabuh ke pengaturan pengguna). Ini juga dapat mencakup data formulir yang sedang diisi, sampai mereka dikirim ke server.



Lebih baik menyimpan data tersebut dalam keadaan komponen, karena mereka mengacaukan penyimpanan global dan mempersulit pekerjaan dengan mereka: Anda perlu menulis tindakan, mengurangi, menginisialisasi negara dan menghapusnya tepat waktu.



Contoh buruk
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;
    }
}




Contoh yang baik
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;




Jarang mengubah data



Ini adalah data yang biasanya tidak berubah di antara pembaruan halaman atau antara kunjungan individu ke halaman oleh pengguna.



Karena toko Redux dibuat kembali ketika halaman di-refresh, tipe data ini harus disimpan di tempat lain: dalam database di server atau di toko lokal di browser.



Ini bisa berupa data dari direktori atau pengaturan khusus. Misalnya, ketika mengembangkan aplikasi yang menggunakan pengaturan khusus, setelah otentikasi pengguna, kami menyimpan pengaturan ini di toko Redux, yang memungkinkan komponen aplikasi untuk menggunakannya tanpa pergi ke server.



Perlu diingat bahwa beberapa data dapat berubah di server tanpa campur tangan pengguna, dan Anda harus mempertimbangkan bagaimana aplikasi Anda akan menanggapinya.



Contoh buruk
// 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;




Contoh yang baik
// 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;
    }
}




Frekuensi penggunaan



Kriteria kedua adalah berapa banyak komponen dalam aplikasi Bereaksi harus memiliki akses ke keadaan yang sama. Semakin banyak komponen menggunakan data yang sama dalam keadaan, semakin banyak manfaat yang Anda dapatkan dari menggunakan toko Redux.



Jika Anda memahami bahwa untuk komponen tertentu atau sebagian kecil aplikasi Anda, status terisolasi, maka lebih baik menggunakan status Bereaksi dari komponen yang terpisah atau komponen HOC.



Kedalaman negara



Dalam aplikasi non-Redux, Bereaksi status data harus disimpan di komponen paling atas (di pohon) yang komponen anak-anaknya perlu mengakses data ini, dengan asumsi kami menghindari menyimpan data yang sama di tempat yang berbeda.



Kadang-kadang data dari keadaan komponen induk diperlukan oleh sejumlah besar komponen anak di berbagai tingkat sarang, yang mengarah pada saling kuat komponen dan penampilan kode yang tidak berguna di dalamnya, yang mahal untuk diedit setiap kali Anda menemukan bahwa komponen anak membutuhkan akses ke data keadaan baru. Dalam kasus seperti itu, lebih masuk akal untuk menyimpan keadaan di Redux dan mengambil data yang diinginkan dari penyimpanan di komponen yang sesuai.



Jika perlu untuk mentransfer data keadaan ke komponen anak di satu atau dua tingkat bersarang, maka Anda dapat melakukan ini tanpa Redux.



Contoh buruk
//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>
    </>
)




Contoh yang baik
// 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)




Komponen yang tidak terikat yang beroperasi pada data yang sama dalam keadaan



Ada situasi ketika beberapa komponen yang relatif tidak berhubungan membutuhkan akses ke keadaan yang sama. Misalnya, aplikasi perlu membuat formulir untuk mengedit profil pengguna dan header, yang juga perlu menampilkan data pengguna.



Tentu saja, Anda dapat pergi ke ekstrem ketika Anda membuat super-komponen tingkat atas yang menyimpan data profil pengguna dan, pertama, mentransfernya ke komponen header dan komponen turunannya, dan kedua, mentransfernya lebih dalam ke pohon, ke komponen pengeditan profil. Pada saat yang sama, panggilan balik juga akan diperlukan dalam formulir pengeditan profil, yang akan dipanggil ketika data pengguna diubah.



Pertama, pendekatan semacam itu cenderung mengarah pada saling kuat komponen, penampilan data yang tidak perlu dan kode yang tidak perlu dalam komponen perantara, yang akan membutuhkan waktu untuk memperbarui dan memelihara.



Kedua, tanpa perubahan kode tambahan, kemungkinan besar Anda akan mendapatkan komponen yang tidak sendiri menggunakan data yang diberikan kepada mereka, tetapi akan diberikan setiap kali data diperbarui, yang akan menyebabkan penurunan kecepatan aplikasi.



Anda dapat membuatnya lebih sederhana: menyimpan data profil pengguna di toko Redux, dan memungkinkan komponen wadah header dan komponen pengeditan profil untuk menerima dan memodifikasi data di toko Redux.



gambar



Contoh buruk
// 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>
    )
}




Contoh yang baik
// 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)




Kemampuan untuk melacak perubahan dalam status



Kasus lain: Anda perlu menerapkan kemampuan untuk membatalkan / mengulang operasi pengguna dalam aplikasi, atau Anda hanya ingin mencatat perubahan status.



Kami memiliki kebutuhan seperti itu ketika mengembangkan perancang tutorial, yang dengannya pengguna dapat menambah dan menyesuaikan blok dengan teks, gambar dan video pada halaman tutorial, dan juga dapat melakukan operasi Undo / Redo.



Dalam kasus ini, Redux adalah solusi hebat karena setiap tindakan yang dibuat adalah perubahan atom ke negara. Redux menyederhanakan semua tugas ini dengan menyimpannya di satu tempat - toko Redux.



Batalkan / ulangi contoh
// 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);




Untuk meringkas



Pertimbangkan menyimpan data di toko Redux dalam kasus berikut:



  1. Jika data ini jarang berubah;
  2. Jika data yang sama digunakan dalam beberapa (lebih dari 2-3) komponen terkait atau dalam komponen yang tidak terkait;
  3. Jika Anda ingin melacak perubahan data.


Dalam semua kasus lain, lebih baik menggunakan status Bereaksi.



PS Terima kasih banyakmamdaxx111 untuk bantuan dalam mempersiapkan artikel!



All Articles