Бібліотека Redux Persist для роботи з localstorage
При роботі з Redux не можна напряму звертатися до localStorage, оскільки ми не можемо бути однозначно впевнені, що отримаємо дані зі сховища раніше ніж, наприклад, буде зарендерено сторінку.
Кешування даних програми на стороні клієнта (у браузері) - одна з центральних тем frontend-розробки. Можна написати кастомну логіку middleware для роботи з state і localStorage або використовувати готову бібліотеку redux-persist, яка зробить це за нас.
Розглянемо порядок роботи з redux-persist.
Спочатку потрібно встановити пакет redux-persist
npm i redux-persist
Робимо потрібні імпорти в наш store.js
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
Перед нашим store вставляємо обʼєкт налаштувань persistConfig для persist.
const persistConfig = {
key: 'root',
storage,
}
Тут root - назва ключа за яким в localStorage будуть зберігатися наші дані. Persist додасть свій префікс і ключ (в даному випадку називатиметься persist:root). Бажано назвати згідно з назвою параметра, який треба зберегти. Наприклад: value. Тоді в localStorage він називатиметься persist:value.
4. Коли із valueSlice ми імпортуємо редʼюсер, то після persistConfig проганяємо його через persistReducer.
import { valueReducer } from './valueSlice';
const persistedValueReducer = persistReducer(persistConfig, valueReducer)
Першим параметром передаємо файл конфігурації, другим - редʼюсер отриманий з valueSlice. І зберігаємо в нову змінну.
В нашому store міняємо редʼюсер на новий (на той який прогнали через persistReducer), який відповідає за параметр, який хочемо зберегти.
export const store = configureStore({
reducer: {
value: persistedValueReducer,
},
});
Нам також через persistStore потрібно прогнати весь наш store.
export const persistor = persistStore(store);
У нашому файлі index.js треба вказати, що ми використовуємо persist при роботі зі store. Для цього в index.js імпортуємо persistor зі store.js. А також імпортуємо PersistGate з 'redux-persist/integration/react'. Після цього весь наш головний компонент App огортаємо в PersistGate із параметрами loading={null} і persistor={persistor}.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from 'components/App';
import './index.css';
import { Provider } from 'react-redux';
import { store, persistor } from './redux/store';
import { PersistGate } from 'redux-persist/integration/react';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);
Persist налаштовано. Але потрібно в правильному форматі зберігати дані в нашому параметрі value. Раніше в localStorage ми могли зберігати безпосередньо обʼєкт, масив, рядок, число, буль тощо (звичайно після прогонки його через JSON.stringify()). Persist же зберігає в localStorage обʼєкт (теж після JSON.stringify()). Перший параметр - буде наш ключ зазначений в persistConfig. Другий - параметр налаштувань _persist самого persist.
{"names":"[{"id":"00001","name":"Олександр"},{"id":"00002","name":"Іван"}]",
"phones":"[{"id":"00001","phone":"123456"}]",
"totalSallary":"1000000",
"_persist":"{"version":-1,"rehydrated":true}"}
Розглянемо цей приклад. В localStorage зберігається обʼєкт з трьома параметрами: names, phones та totalSallary. Перші два це масиви, третій - рядок і плюс вкінці службовий параметр від самого persist.
В такому разі розглянемо формат об'єкта нашого параметра. На прикладі початкового значення initialState (якщо в localStorage уже чи ще нічого немає)
const initialState = {
names: [
{ id: '00001', name: 'Олександр' },
{ id: '00002', name: 'Іван' },
],
phones: [{ id: '00001', phone: '123456' }],
totalSallary: '1000000',
};
В такому разі коли ми захочемо у редʼюсері чи селекторі звертатися до якогось параметра всередині нашого стейта - робимо це через крапку і назву параметра. Відповідно: state.names state.phones. state.totalSallary.
За замовчанням всі параметри обʼєкта зберігатимуться в localStorage. Але ми можемо використовувати додаткові налаштування для вибору, що зберігати. Blacklist - додає якийсь параметр обʼєкта в виключення (збережуться всі крім нього), або Whitelist - перераховуємо тільки ті параметри, які плануємо зберігати в localStorage.
const persistConfig = {
key: 'value',
storage,
blacklist: ['phones'] // phones will not be persisted
}
const persistConfig = {
key: 'value',
storage,
whitelist: ['phones'] // only phones will be persisted
}
Рефакторинг коду
Як бачимо із вищезазначеного коду, що наш файл store містить інформацію, яка більше стосується редʼюсера, а не самого стетйту. Тому логічніше persistConfig і persistedReducer винести в файл slice і звідти його експортувати в store.js.
import storage from 'redux-persist/lib/storage';
import { persistReducer } from 'redux-persist';
// Код
const persistConfig = {
key: 'value',
storage,
whitelist: ['names', 'phones'],
};
export const persistedValueReducer = persistReducer(
persistConfig,
valueSlice.reducer
);
import { persistedValueReducer } from '../redux/valueSlice';
Нижче наведено код всіх повʼязаних файлів після рефакторингу
import { createSlice } from '@reduxjs/toolkit';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import { persistReducer } from 'redux-persist';
const initialState = {
names: [],
phones: [],
totalSallary: '',
};
const valueSlice = createSlice({
name: 'value',
initialState,
reducers: {
// тут пишемо всі редʼюсери нашого стейту (для прикладу два)
addName(state, action) {
state.names.push(action.payload);
},
deleteName(state, action) {
const index = state.names.findIndex(
name => name.id === action.payload.id
);
state.name.splice(index, 1);
},
},
});
// конфігурація persist
const persistConfig = {
key: 'value',
storage,
whitelist: ['names', 'phones'],
};
// пропускаємо редʼюсер через персіст
export const persistedValueReducer = persistReducer(
persistConfig,
valueSlice.reducer
);
// експорти наших екшенів (для підключення в компонентах)
export const { addName, deleteName } = valueSlice.actions;
//селектори для отримання стейту в компонентах
export const selectNames = state => state.value.names;
import { configureStore } from '@reduxjs/toolkit';
import { persistedValueReducer } from '../redux/valueSlice';
import { persistStore } from 'redux-persist';
export const store = configureStore({
reducer: {
value: persistedValueReducer,
},
});
// експортуємо стор прогнаний через персістор
export const persistor = persistStore(store);
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from 'components/App';
import './index.css';
import { Provider } from 'react-redux';
import { store, persistor } from './redux/store';
import { PersistGate } from 'redux-persist/integration/react';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);
Внизу valueSlice.js бачимо експорти екшенів та селекторів. Їх ми уже безпосередньо використовуємо, коли звертаємося в компонентах до стейту.
Селектори використовуємо для того, щоб отримати в змінну значення стейту.
import { useSelector } from 'react-redux';
import { selectNames } from 'redux/valueSlice';
// Код компоненту
const name = useSelector(selectNames);
Екшени формують обʼєкт "завдання", яке до редʼюсера доправляє діспатч.
import { useDispatch } from 'react-redux';
import { addName } from 'redux/valueSlice';
// Код компоненту
const dispatch = useDispatch();
// Обʼєкт нового імені
const newName = {id: '00003', name: 'John'}
// Код у верстці компоненту
<Button
type="button"
onClick={() => dispatch(addName(newName))}
>
Add name
</Button>
Посилання на офіційну документацію:
Last updated