Skip to content

Commit ba6e5b1

Browse files
Merge pull request #5 from FancyPixel/1-persist-only-a-part-of-a-slice-or-reducer
Fix #1, #2, #3
2 parents edf3751 + f5c9d9f commit ba6e5b1

9 files changed

Lines changed: 615 additions & 105 deletions

File tree

‎README.md‎

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<br />
88

9-
# RTK Persist [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) ![GitHub package.json version](https://img.shields.io/github/package-json/v/FancyPixel/rtk-persist?color=%2332C553)
9+
# RTK Persist
1010

1111
**`rtk-persist`** is a lightweight, zero-dependency library that enhances Redux Toolkit's state management by adding seamless, persistent storage. It allows specified slices or reducers of your Redux state to be saved to a storage medium of your choice (like `localStorage` or `AsyncStorage`) and rehydrated on app startup.
1212

@@ -17,13 +17,23 @@ The library works by wrapping standard Redux Toolkit functions, adding persisten
1717
## ✨ Features
1818

1919
* **Effortless Persistence**: Persist any Redux Toolkit slice or reducer with minimal configuration.
20+
2021
* **Asynchronous Rehydration**: Store creation is now asynchronous, ensuring that your app only renders after the state has been fully rehydrated.
22+
2123
* **Seamless Integration**: Designed as a drop-in replacement for RTK functions. Adding or removing persistence is as simple as changing an import.
24+
2225
* **React Redux Integration**: Comes with a `<PersistedProvider />` and a `usePersistedStore` hook for easy integration with React applications.
26+
2327
* **Flexible API**: Choose between a `createPersistedSlice` utility or a `createPersistedReducer` builder syntax.
28+
2429
* **Nested State Support**: Easily persist slices or reducers that are deeply nested within your root state using a simple `nestedPath` option.
30+
31+
* **Custom Serialization**: Use `onPersist` and `onRehydrate` to transform your state before saving and after loading.
32+
2533
* **Storage Agnostic**: Works with any storage provider that implements a simple `getItem`, `setItem`, and `removeItem` interface.
34+
2635
* **TypeScript Support**: Fully typed to ensure a great developer experience with path validation.
36+
2737
* **Minimal Footprint**: Extremely lightweight with a production size under 15 KB.
2838

2939
<br />
@@ -83,6 +93,7 @@ export const counterSlice = createPersistedSlice({
8393

8494
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
8595
export default counterSlice.reducer;
96+
8697
```
8798

8899
### Option 2: Using `createPersistedReducer`
@@ -114,6 +125,7 @@ export const counterReducer = createPersistedReducer(
114125
});
115126
}
116127
);
128+
117129
```
118130

119131
### 2. Configure the Store
@@ -147,6 +159,7 @@ export const store = configurePersistedStore(
147159
export type Store = Awaited<typeof store>;
148160
export type RootState = ReturnType<Store['getState']>;
149161
export type AppDispatch = Store['dispatch'];
162+
150163
```
151164

152165
<br />
@@ -163,7 +176,7 @@ This component replaces the standard `Provider` from `react-redux`. It waits for
163176

164177
In your application's entry point (e.g., `main.tsx` or `index.js`), wrap your `App` component with `PersistedProvider`.
165178

166-
```tsx
179+
```typescript
167180
// main.tsx
168181
import React from 'react';
169182
import ReactDOM from 'react-dom/client';
@@ -178,62 +191,38 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
178191
</PersistedProvider>
179192
</React.StrictMode>,
180193
);
194+
181195
```
182196

183197
The `PersistedProvider` accepts two props:
198+
184199
* `store`: The promise returned by `configurePersistedStore`.
200+
185201
* `loader` (optional): A React node to display while the store is rehydrating.
186202

187203
### `usePersistedStore`
188204

189-
A custom hook that provides access to the rehydrated store instance. This is useful for dispatching actions or accessing store methods like `flush`.
205+
A custom hook that provides access to the rehydrated store instance. This is useful for dispatching actions or accessing store methods.
190206

191207
#### Usage
192208

193-
```tsx
209+
```typescript
194210
import React from 'react';
195211
import { usePersistedStore } from 'rtk-persist';
196212

197213
const MyComponent = () => {
198214
const { store } = usePersistedStore();
199215

200-
const handleSaveNow = () => {
201-
// Manually forces the store to save its current state to storage.
202-
store.flush();
216+
const handleClear = () => {
217+
// Manually clears the persisted state from storage.
218+
store.clearPersistedState();
203219
};
204220

205-
return <button onClick={handleSaveNow}>Save Now</button>;
221+
return <button onClick={handleClear}>Clear Persisted State</button>;
206222
};
207-
```
208-
209-
<br />
210-
211-
## ↔️ Seamless Integration
212223

213-
A core design principle of `rtk-persist` is that it should be easy to add or remove. The API is intentionally designed to mirror Redux Toolkit's, so enabling or disabling persistence is as simple as changing an import.
214-
215-
**From this:**
216-
217-
```typescript
218-
import { createSlice } from '@reduxjs/toolkit';
219-
220-
export const counterSlice = createSlice({
221-
/* ... */
222-
});
223224
```
224225

225-
**To this:**
226-
227-
```typescript
228-
import { createPersistedSlice } from 'rtk-persist';
229-
230-
export const counterSlice = createPersistedSlice({
231-
/* ... */
232-
});
233-
```
234-
235-
No other code changes are needed in your slice file.
236-
237226
<br />
238227

239228
## 🌳 Handling Nested State
@@ -256,7 +245,9 @@ export const counterSlice = createPersistedSlice(
256245
/* ... */
257246
},
258247
},
259-
'features.counter' // The nestedPath to the slice's state
248+
{
249+
nestedPath: 'features.counter' // The nestedPath to the slice's state
250+
}
260251
);
261252

262253
// app/store.ts
@@ -277,10 +268,74 @@ export const store = configurePersistedStore(
277268
'my-app-id',
278269
localStorage
279270
);
271+
280272
```
281273

282274
<br />
283275

276+
## 🔬 Advanced Usage: Custom Serialization
277+
278+
Sometimes, you may need to transform a slice's state before it's saved to storage or after it's rehydrated. For example, you might want to store a `Date` object as an ISO string, or omit certain transient properties.
279+
280+
`rtk-persist` supports this through the `onPersist` and `onRehydrate` options.
281+
282+
### Example with `onPersist` and `onRehydrate`
283+
284+
Here's how you can persist a slice that contains a non-serializable value like a `Date` object.
285+
286+
```typescript
287+
// features/session/sessionSlice.ts
288+
import { createPersistedSlice } from 'rtk-persist';
289+
290+
interface SessionState {
291+
lastLogin: Date | null;
292+
token: string | null;
293+
}
294+
295+
const initialState: SessionState = {
296+
lastLogin: null,
297+
token: null,
298+
};
299+
300+
export const sessionSlice = createPersistedSlice(
301+
{
302+
name: 'session',
303+
initialState,
304+
reducers: {
305+
login: (state, action) => {
306+
state.token = action.payload.token;
307+
state.lastLogin = new Date();
308+
},
309+
logout: (state) => {
310+
state.token = null;
311+
state.lastLogin = null;
312+
},
313+
},
314+
},
315+
{
316+
// Transform state before saving
317+
onPersist: (state) => ({
318+
...state,
319+
lastLogin: state.lastLogin ? state.lastLogin.toISOString() : null,
320+
}),
321+
// Transform state after rehydrating
322+
onRehydrate: (state) => ({
323+
...state,
324+
lastLogin: state.lastLogin ? new Date(state.lastLogin) : null,
325+
}),
326+
}
327+
);
328+
329+
```
330+
331+
In this example:
332+
333+
* `onPersist` converts the `lastLogin` `Date` object into an ISO string before it's written to `localStorage`.
334+
335+
* `onRehydrate` parses the ISO string and converts it back into a `Date` object when the state is loaded from storage.
336+
337+
<br />
338+
284339
## 🛠️ API
285340

286341
### `createPersistedSlice`
@@ -290,13 +345,18 @@ A wrapper around RTK's `createSlice` that adds persistence.
290345
#### Takes
291346

292347
* **`sliceOptions`**: The standard `CreateSliceOptions` object from Redux Toolkit.
293-
* **`nestedPath`** (optional, `string`): A dot-notation string representing the path to the slice's state from the root. Required if the slice is not at the root level.
294348

295-
#### Returns
349+
* **`persistenceOptions`** (optional, `object`): Configuration for persistence behavior.
350+
351+
* `nestedPath` (optional, `string`): A dot-notation string for the slice's state if it's nested.
352+
353+
* `onPersist` (optional, `function`): A function to transform state *before* it's saved.
354+
355+
* `onRehydrate` (optional, `function`): A function to transform state *after* it's rehydrated.
296356

297-
* A standard `Slice` object, enhanced with a `nestedPath` property.
357+
#### Returns
298358

299-
---
359+
* A `PersistedSlice` object, which is a standard `Slice` object enhanced with persistence properties.
300360

301361
### `createPersistedReducer`
302362

@@ -305,15 +365,22 @@ A wrapper around RTK's `createReducer` that adds persistence.
305365
#### Takes
306366

307367
* **`name`**: A unique string to identify this reducer in storage.
368+
308369
* **`initialState`**: The initial state for the reducer.
370+
309371
* **`builderCallback`**: A callback that receives a `builder` object to define case reducers.
310-
* **`nestedPath`** (optional, `string`): A dot-notation string representing the path to the reducer's state. An empty string (`''`) signifies that this reducer is the root state.
311372

312-
#### Returns
373+
* **`persistenceOptions`** (optional, `object`): Configuration for persistence behavior.
374+
375+
* `nestedPath` (optional, `string`): A dot-notation string for the reducer's state. An empty string (`''`) signifies the root state.
313376

314-
* A standard `Reducer` function, enhanced with `reducerName` and `nestedPath` properties.
377+
* `onPersist` (optional, `function`): A function to transform state *before* it's saved.
315378

316-
---
379+
* `onRehydrate` (optional, `function`): A function to transform state *after* it's rehydrated.
380+
381+
#### Returns
382+
383+
* A `PersistedReducer` function, which is a standard `Reducer` enhanced with persistence properties.
317384

318385
### `configurePersistedStore`
319386

@@ -322,16 +389,22 @@ A wrapper around RTK's `configureStore`.
322389
#### Takes
323390

324391
* **`storeOptions`**: The standard `ConfigureStoreOptions` object.
392+
325393
* **`applicationId`**: A unique string that identifies the application to namespace storage keys.
394+
326395
* **`storageHandler`**: A storage object that implements `getItem`, `setItem`, and `removeItem`.
396+
327397
* **`persistenceOptions`** (optional): An object to control the persistence behavior:
328-
* `rehydrationTimeout` (optional, `number`): Max time in ms to wait for rehydration. Defaults to `5000`.
398+
399+
* `rehydrationTimeout` (optional, `number`): Max time in ms to wait for rehydration. Defaults to `5000`.
329400

330401
#### Returns
331402

332403
* A `Promise<PersistedStore>` object, which resolves to a standard Redux store enhanced with the following methods:
333-
* **`rehydrate()`**: A function to manually trigger rehydration from storage.
334-
* **`clearPersistedState()`**: A function that clears all persisted data for the application from storage.
404+
405+
* **`rehydrate()`**: A function to manually trigger rehydration from storage.
406+
407+
* **`clearPersistedState()`**: A function that clears all persisted data for the application from storage.
335408

336409
<br />
337410

@@ -346,5 +419,5 @@ This library was crafted from our daily experiences building modern web and mobi
346419
## 📄 License
347420

348421
This project is licensed under the MIT License.
349-
422+
350423
Library icon freely created from a [iconsax](https://iconsax.io/) icon and the [redux](https://redux.js.org/img/redux.svg) logo.

0 commit comments

Comments
 (0)