Di mana harus menulis ke localStorage di aplikasi Redux?

166

Saya ingin bertahan beberapa bagian pohon negara saya ke Penyimpanan lokal. Apa tempat yang tepat untuk melakukannya? Peredam atau aksi?

Marco de Jongh
sumber

Jawaban:

223

Peredam tidak pernah merupakan tempat yang tepat untuk melakukan ini karena peredam harus murni dan tidak memiliki efek samping.

Saya akan merekomendasikan hanya melakukannya di pelanggan:

store.subscribe(() => {
  // persist your state
})

Sebelum membuat toko, baca bagian-bagian yang bertahan:

const persistedState = // ...
const store = createStore(reducer, persistedState)

Jika Anda menggunakan combineReducers()Anda akan melihat bahwa reduksi yang belum menerima status akan "boot up" seperti biasa menggunakan standarnyastate nilai argumen . Ini bisa sangat berguna.

Dianjurkan agar Anda debounce pelanggan Anda sehingga Anda tidak menulis ke localStorage terlalu cepat, atau Anda akan memiliki masalah kinerja.

Akhirnya, Anda dapat membuat middleware yang merangkumnya sebagai alternatif, tetapi saya akan mulai dengan pelanggan karena ini adalah solusi yang lebih sederhana dan melakukan pekerjaan dengan baik.

Dan Abramov
sumber
1
bagaimana jika saya ingin berlangganan hanya bagian dari negara? Apakah itu mungkin? Saya pergi ke depan dengan sesuatu yang sedikit berbeda: 1. menyelamatkan keadaan yang bertahan di luar redux. 2. memuat status tetap di dalam konstruktor reaksi (atau dengan componentWillMount) dengan aksi redux. Apakah 2 benar-benar baik-baik saja daripada memuat data yang ada di toko secara langsung? (yang saya coba simpan secara terpisah untuk SSR). Omong-omong, terima kasih untuk Redux! Ini luar biasa, saya menyukainya, setelah tersesat dengan kode proyek besar saya sekarang semuanya kembali ke yang sederhana dan dapat diprediksi :)
Mark Uretsky
dalam callback store.subscribe, Anda memiliki akses penuh ke data toko saat ini, sehingga Anda dapat bertahan bagian mana pun yang Anda tertarik
Bo Chen
35
Dan Abramov juga telah membuat seluruh video di dalamnya: egghead.io/lessons/…
NateW
2
Apakah ada masalah kinerja yang sah dengan membuat serial keadaan pada setiap pembaruan toko? Apakah peramban bersambung pada utas yang terpisah mungkin?
Stephen Paul
7
Ini terlihat berpotensi boros. OP memang mengatakan bahwa hanya sebagian dari negara yang perlu dipertahankan. Katakanlah Anda memiliki toko dengan 100 kunci berbeda. Anda hanya ingin bertahan 3 dari mereka yang jarang berubah. Sekarang Anda mem-parsing dan memperbarui toko lokal pada setiap perubahan kecil ke salah satu dari 100 kunci Anda, sementara itu tidak satu pun dari 3 kunci yang Anda tertarik untuk bertahan bahkan pernah diubah. Solusi oleh @Gardezi di bawah ini adalah pendekatan yang lebih baik karena Anda dapat menambahkan acara pendengar ke middleware Anda sehingga Anda hanya memperbarui ketika Anda benar-benar perlu misalnya: codepen.io/retrcult/pen/qNRzKN
Anna T
153

Untuk mengisi kekosongan jawaban Dan Abramov, Anda dapat menggunakan store.subscribe()seperti ini:

store.subscribe(()=>{
  localStorage.setItem('reduxState', JSON.stringify(store.getState()))
})

Sebelum membuat toko, periksa localStoragedan uraikan JSON apa pun di bawah kunci Anda seperti ini:

const persistedState = localStorage.getItem('reduxState') 
                       ? JSON.parse(localStorage.getItem('reduxState'))
                       : {}

Anda kemudian meneruskan persistedStatekonstanta ini ke createStoremetode Anda seperti ini:

const store = createStore(
  reducer, 
  persistedState,
  /* any middleware... */
)
Andrew Samuelsen
sumber
6
Sederhana dan efektif, tanpa ketergantungan tambahan.
AxeEffect
1
membuat middleware mungkin terlihat sedikit lebih baik, tetapi saya setuju bahwa itu efektif dan cukup untuk sebagian besar kasus
Bo Chen
Apakah ada cara yang bagus untuk melakukan ini ketika menggunakan CombedReducers, dan Anda hanya ingin bertahan satu toko?
brendangibson
1
Jika tidak ada yang ada di localStorage, haruskah persistedStatemengembalikan initialStateobjek kosong? Kalau tidak, saya pikir createStoreakan menginisialisasi dengan objek kosong itu.
Alex
1
@Alex Anda benar, kecuali kondisi awal Anda kosong.
Link14
47

Dalam sebuah kata: middleware.

Lihat redux-persisten . Atau tulis sendiri.

[UPDATE 18 Des 2016] Diedit untuk menghapus penyebutan dua proyek serupa yang sekarang tidak aktif atau usang.

David L. Walsh
sumber
12

Jika ada yang mengalami masalah dengan solusi di atas, Anda dapat menulis sendiri. Mari saya tunjukkan apa yang saya lakukan. Abaikan saga middlewarehal-hal hanya fokus pada dua hal localStorageMiddlewaredan reHydrateStoremetode. yang localStorageMiddlewaremenarik semua redux statedan menempatkan dalam local storagedan rehydrateStoremenarik semua applicationStatedi penyimpanan lokal jika ada dan menempatkan dalamredux store

import {createStore, applyMiddleware} from 'redux'
import createSagaMiddleware from 'redux-saga';
import decoristReducers from '../reducers/decorist_reducer'

import sagas from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

/**
 * Add all the state in local storage
 * @param getState
 * @returns {function(*): function(*=)}
 */
const localStorageMiddleware = ({getState}) => { // <--- FOCUS HERE
    return (next) => (action) => {
        const result = next(action);
        localStorage.setItem('applicationState', JSON.stringify(
            getState()
        ));
        return result;
    };
};


const reHydrateStore = () => { // <-- FOCUS HERE

    if (localStorage.getItem('applicationState') !== null) {
        return JSON.parse(localStorage.getItem('applicationState')) // re-hydrate the store

    }
}


const store = createStore(
    decoristReducers,
    reHydrateStore(),// <-- FOCUS HERE
    applyMiddleware(
        sagaMiddleware,
        localStorageMiddleware,// <-- FOCUS HERE 
    )
)

sagaMiddleware.run(sagas);

export default store;
Gardezi
sumber
2
Hai, bukankah ini akan menghasilkan banyak penulisan localStoragewalaupun tidak ada yang berubah di toko? Bagaimana Anda mengimbangi penulisan yang tidak perlu
user566245
Yah, itu berhasil, lagipula ide bagus ini untuk dimiliki? Pertanyaan: Apa yang terjadi jika kita memiliki lebih banyak data menas beberapa reduksi dengan data besar?
Kunvar Singh
3

Saya tidak bisa menjawab @Gardezi tetapi opsi berdasarkan kodenya bisa:

const rootReducer = combineReducers({
    users: authReducer,
});

const localStorageMiddleware = ({ getState }) => {
    return next => action => {
        const result = next(action);
        if ([ ACTIONS.LOGIN ].includes(result.type)) {
            localStorage.setItem(appConstants.APP_STATE, JSON.stringify(getState()))
        }
        return result;
    };
};

const reHydrateStore = () => {
    const data = localStorage.getItem(appConstants.APP_STATE);
    if (data) {
        return JSON.parse(data);
    }
    return undefined;
};

return createStore(
    rootReducer,
    reHydrateStore(),
    applyMiddleware(
        thunk,
        localStorageMiddleware
    )
);

perbedaannya adalah bahwa kami hanya menyimpan beberapa tindakan, Anda dapat menggunakan fungsi debounce untuk menyimpan hanya interaksi terakhir dari negara Anda

Douglas Caina
sumber
1

Saya agak terlambat tetapi saya menerapkan kondisi persisten sesuai dengan contoh yang dinyatakan di sini. Jika Anda ingin memperbarui negara hanya setiap X detik, pendekatan ini dapat membantu Anda:

  1. Tentukan fungsi pembungkus

    let oldTimeStamp = (Date.now()).valueOf()
    const millisecondsBetween = 5000 // Each X milliseconds
    function updateLocalStorage(newState)
    {
        if(((Date.now()).valueOf() - oldTimeStamp) > millisecondsBetween)
        {
            saveStateToLocalStorage(newState)
            oldTimeStamp = (Date.now()).valueOf()
            console.log("Updated!")
        }
    }
  2. Panggil fungsi wrapper di pelanggan Anda

        store.subscribe((state) =>
        {
        updateLocalStorage(store.getState())
         });

Dalam contoh ini, negara diperbarui paling banyak setiap 5 detik, terlepas dari seberapa sering pembaruan dipicu.

movcmpret
sumber
1
Anda bisa membungkus (state) => { updateLocalStorage(store.getState()) }di lodash.throttleseperti ini: store.subscribe(throttle(() => {(state) => { updateLocalStorage(store.getState())} }dan menghapus waktu memeriksa logika dalam.
GeorgeMA
1

Mengembangkan saran yang sangat baik dan kutipan kode pendek yang disediakan dalam jawaban lain (dan artikel Medium Jam Creencia ), inilah solusi lengkap!

Kami membutuhkan file yang berisi 2 fungsi yang menyimpan / memuat status ke / dari penyimpanan lokal:

// FILE: src/common/localStorage/localStorage.js

// Pass in Redux store's state to save it to the user's browser local storage
export const saveState = (state) =>
{
  try
  {
    const serializedState = JSON.stringify(state);
    localStorage.setItem('state', serializedState);
  }
  catch
  {
    // We'll just ignore write errors
  }
};



// Loads the state and returns an object that can be provided as the
// preloadedState parameter of store.js's call to configureStore
export const loadState = () =>
{
  try
  {
    const serializedState = localStorage.getItem('state');
    if (serializedState === null)
    {
      return undefined;
    }
    return JSON.parse(serializedState);
  }
  catch (error)
  {
    return undefined;
  }
};

Fungsi-fungsi itu diimpor oleh store.js tempat kami mengonfigurasi toko kami:

CATATAN: Anda harus menambahkan satu ketergantungan: npm install lodash.throttle

// FILE: src/app/redux/store.js

import { configureStore, applyMiddleware } from '@reduxjs/toolkit'

import throttle from 'lodash.throttle';

import rootReducer from "./rootReducer";
import middleware from './middleware';

import { saveState, loadState } from 'common/localStorage/localStorage';


// By providing a preloaded state (loaded from local storage), we can persist
// the state across the user's visits to the web app.
//
// READ: https://redux.js.org/recipes/configuring-your-store
const store = configureStore({
	reducer: rootReducer,
	middleware: middleware,
	enhancer: applyMiddleware(...middleware),
	preloadedState: loadState()
})


// We'll subscribe to state changes, saving the store's state to the browser's
// local storage. We'll throttle this to prevent excessive work.
store.subscribe(
	throttle( () => saveState(store.getState()), 1000)
);


export default store;

Toko diimpor ke index.js sehingga dapat dilewatkan ke Penyedia yang membungkus App.js :

// FILE: src/index.js

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import App from './app/core/App'

import store from './app/redux/store';


// Provider makes the Redux store available to any nested components
render(
	<Provider store={store}>
		<App />
	</Provider>,
	document.getElementById('root')
)

Perhatikan bahwa impor absolut memerlukan perubahan ini ke YourProjectFolder / jsconfig.json - ini memberi tahu tempat untuk mencari file jika tidak dapat menemukannya pada awalnya. Jika tidak, Anda akan melihat keluhan tentang upaya mengimpor sesuatu dari luar src .

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

CanProgram
sumber