Skip to content

Commit 0d50402

Browse files
Improve docs and tests
1 parent 962976e commit 0d50402

17 files changed

+1431
-1199
lines changed

src/extraReducersBuilder.ts

Lines changed: 41 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Action, ActionReducerMapBuilder, CaseReducer, Draft } from "@reduxjs/toolkit";
1+
import {
2+
Action,
3+
ActionReducerMapBuilder,
4+
CaseReducer,
5+
Draft,
6+
} from '@reduxjs/toolkit';
27

38
/**
49
* A higher-order reducer that wraps a case reducer and invokes a callback
@@ -9,23 +14,33 @@ import { Action, ActionReducerMapBuilder, CaseReducer, Draft } from "@reduxjs/to
914
* @returns The wrapped case reducer.
1015
* @internal
1116
*/
12-
const persistReducer = <SliceState>(r: CaseReducer<SliceState, Action>, onStateUpdate: (s: Draft<SliceState>, a: Action) => void) => (s: Draft<SliceState>, a: Action): void | SliceState | Draft<SliceState> => {
13-
const ns = r(s, a);
14-
onStateUpdate(s, a);
15-
if (ns) return ns;
16-
};
17+
const persistReducer =
18+
<SliceState>(
19+
r: CaseReducer<SliceState, Action>,
20+
onStateUpdate: (s: Draft<SliceState>, a: Action) => void,
21+
) =>
22+
(
23+
s: Draft<SliceState>,
24+
a: Action,
25+
): void | SliceState | Draft<SliceState> => {
26+
const ns = r(s, a);
27+
onStateUpdate(s, a);
28+
if (ns) return ns;
29+
};
1730

1831
/**
19-
* A wrapper for the `ActionReducerMapBuilder` that automatically hooks into
20-
* state changes to track updates for persistence. Every reducer added via this
21-
* builder will be wrapped to call the `onStateUpdate` callback.
32+
* A proxy for the `ActionReducerMapBuilder` that intercepts calls to `addCase`,
33+
* `addMatcher`, and `addDefaultCase`. It wraps the provided reducers with
34+
* persistence logic, ensuring that the `onStateUpdate` callback is triggered
35+
* after any state change.
2236
*
2337
* @param builder The original `ActionReducerMapBuilder` from Redux Toolkit.
24-
* @param onStateUpdate The callback to invoke after any case reducer has been executed. It receives the state draft and the action.
25-
*
38+
* @param onStateUpdate The callback to invoke after any case reducer has been
39+
* executed. It receives the state draft and the action.
2640
* @internal
2741
*/
28-
export class Builder<SliceState> implements ActionReducerMapBuilder<SliceState>
42+
export class Builder<SliceState>
43+
implements ActionReducerMapBuilder<SliceState>
2944
{
3045
builder: ActionReducerMapBuilder<SliceState>;
3146
onStateUpdate: (s: Draft<SliceState>, a: Action) => void;
@@ -44,95 +59,33 @@ export class Builder<SliceState> implements ActionReducerMapBuilder<SliceState>
4459
}
4560

4661
/**
47-
* Adds a "default case" reducer that is executed if no case reducer and no matcher
48-
* reducer was executed for this action.
49-
* @param reducer - The fallback "default case" reducer function.
50-
*
51-
* @example
52-
```ts
53-
import { createReducer } from '@reduxjs/toolkit'
54-
const initialState = { otherActions: 0 }
55-
const reducer = createReducer(initialState, builder => {
56-
builder
57-
// .addCase(...)
58-
// .addMatcher(...)
59-
.addDefaultCase((state, action) => {
60-
state.otherActions++
61-
})
62-
})
63-
```
64-
*/
62+
* Wraps and adds a "default case" reducer. This behaves like the
63+
* original `builder.addDefaultCase` but also triggers the persistence
64+
* callback upon execution.
65+
* @param reducer - The fallback "default case" reducer function.
66+
*/
6567
addDefaultCase(r: CaseReducer<SliceState, Action>) {
6668
this.builder.addDefaultCase(persistReducer(r, this.onStateUpdate));
6769
return this;
6870
}
6971

7072
/**
71-
* Allows you to match your incoming actions against your own filter function instead of only the `action.type` property.
72-
* @remarks
73-
* If multiple matcher reducers match, all of them will be executed in the order
74-
* they were defined in - even if a case reducer already matched.
75-
* All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
76-
* @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
77-
* function
73+
* Wraps and adds a matcher reducer. This behaves like the original
74+
* `builder.addMatcher` but also triggers the persistence callback
75+
* upon execution.
76+
* @param matcher - A matcher function to filter actions.
7877
* @param reducer - The actual case reducer function.
79-
*
80-
* @example
81-
```ts
82-
import {
83-
createAction,
84-
createReducer,
85-
AsyncThunk,
86-
UnknownAction,
87-
} from "@reduxjs/toolkit";
88-
89-
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
90-
91-
type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
92-
type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
93-
type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
94-
95-
const initialState: Record<string, string> = {};
96-
const resetAction = createAction("reset-tracked-loading-state");
97-
98-
function isPendingAction(action: UnknownAction): action is PendingAction {
99-
return typeof action.type === "string" && action.type.endsWith("/pending");
100-
}
101-
102-
const reducer = createReducer(initialState, (builder) => {
103-
builder
104-
.addCase(resetAction, () => initialState)
105-
// matcher can be defined outside as a type predicate function
106-
.addMatcher(isPendingAction, (state, action) => {
107-
state[action.meta.requestId] = "pending";
108-
})
109-
.addMatcher(
110-
// matcher can be defined inline as a type predicate function
111-
(action): action is RejectedAction => action.type.endsWith("/rejected"),
112-
(state, action) => {
113-
state[action.meta.requestId] = "rejected";
114-
}
115-
)
116-
// matcher can just return boolean and the matcher can receive a generic argument
117-
.addMatcher<FulfilledAction>(
118-
(action) => action.type.endsWith("/fulfilled"),
119-
(state, action) => {
120-
state[action.meta.requestId] = "fulfilled";
121-
}
122-
);
123-
});
124-
```
125-
*/
78+
*/
12679
addMatcher(p: any, r: CaseReducer<SliceState, any>) {
12780
this.builder.addMatcher(p, persistReducer(r, this.onStateUpdate));
12881
return this;
12982
}
13083

13184
/**
132-
* Adds a case reducer to handle a single exact action type.
133-
* @remarks
134-
* All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
135-
* @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
85+
* Wraps and adds a case reducer to handle a single action type. This
86+
* behaves like the original `builder.addCase` but also triggers the
87+
* persistence callback upon execution.
88+
* @param actionCreator - The action creator or type string to match against.
13689
* @param reducer - The actual case reducer function.
13790
*/
13891
addCase(ac: string, r: CaseReducer<SliceState, any>) {

src/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
/**
22
* @module rtk-persist
3-
* @description This is the main entry point for the rtk-persist library.
4-
* It exports the core functions needed to add persistence capabilities
5-
* to a Redux Toolkit application.
3+
* @description
4+
* The main entry point for the `rtk-persist` library. This module exports the
5+
* necessary functions to seamlessly integrate persistence into your Redux Toolkit
6+
* applications. Whether you're setting up a new store, creating individual
7+
* persisted slices, or wrapping existing reducers, these utilities provide
8+
* a simple and efficient way to save and rehydrate your Redux state.
69
*/
710
import { createPersistedReducer } from './reducer';
8-
import { createPersistedSlice } from "./slice";
9-
import { configurePersistedStore } from "./store";
11+
import { createPersistedSlice } from './slice';
12+
import { configurePersistedStore } from './store';
1013

1114
export {
15+
/**
16+
* A wrapper around Redux Toolkit's `configureStore` that automatically
17+
* sets up the persistence middleware and initial state rehydration.
18+
* Use this for a quick and easy setup of a fully persisted store.
19+
*/
1220
configurePersistedStore,
21+
/**
22+
* A utility to wrap an existing reducer with persistence logic. This is
23+
* useful when you need to add persistence to a reducer that was not
24+
* created with `createPersistedSlice`.
25+
*/
1326
createPersistedReducer,
27+
/**
28+
* A wrapper around Redux Toolkit's `createSlice` that adds persistence
29+
* capabilities. Slices created with this function will automatically
30+
* have their state saved to storage on every change.
31+
*/
1432
createPersistedSlice
1533
};

src/middleware.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { createListenerMiddleware } from "@reduxjs/toolkit";
1+
import { createListenerMiddleware } from '@reduxjs/toolkit';
22

33
/**
4-
* The single, global instance of the RTK listener middleware.
5-
* This middleware is used to listen for specific actions and trigger
6-
* side effects, such as persisting the state to storage.
4+
* The core listener middleware for `rtk-persist`.
5+
*
6+
* This singleton instance is central to the persistence mechanism. It's used
7+
* to listen for actions that modify the state of persisted slices and triggers
8+
* the side effect of writing those changes to storage.
79
*
810
* {@link https://redux-toolkit.js.org/api/createListenerMiddleware}
911
*

0 commit comments

Comments
 (0)