Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/action config (ability to pass a config object and disable immer) #628 #781

Merged
merged 10 commits into from
Nov 13, 2022
13 changes: 11 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,14 @@ export type Action<Model extends object, Payload = void> = {
result: void | State<Model>;
};


/**
* @param {boolean} [immer=true] - If true, the action will be wrapped in an immer produce call. Otherwise, the action will update the state directly.
**/
interface Config {
immer?: boolean;
}

/**
* Declares an action.
*
Expand All @@ -636,7 +644,7 @@ export type Action<Model extends object, Payload = void> = {
* });
*/
export function action<Model extends object = {}, Payload = any>(
action: (state: State<Model>, payload: Payload) => void | State<Model>,
action: (state: State<Model>, payload: Payload, config?: Config) => void | State<Model>,
): Action<Model, Payload>;

// #endregion
Expand All @@ -661,6 +669,7 @@ export function actionOn<
state: State<Model>,
target: TargetPayload<PayloadFromResolver<Resolver>>,
) => void | State<Model>,
config?: Config,
): ActionOn<Model, StoreModel>;

// #endregion
Expand Down Expand Up @@ -884,7 +893,7 @@ export type Reducer<State = any, Action extends ReduxAction = AnyAction> = {
* })
* });
*/
export function reducer<State>(state: ReduxReducer<State>): Reducer<State>;
export function reducer<State>(state: ReduxReducer<State>, config?: Config): Reducer<State>;

// #endregion

Expand Down
1 change: 1 addition & 0 deletions src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function createActionCreator(def, _r) {
const action = {
type: def.meta.type,
payload,
config: def.config,
};
if (def[actionOnSymbol] && def.meta.resolvedTargets) {
payload.resolvedTargets = [...def.meta.resolvedTargets];
Expand Down
9 changes: 5 additions & 4 deletions src/create-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { createSimpleProduce, get } from './lib';
export default function createReducer(disableImmer, _aRD, _cR, _cP) {
const simpleProduce = createSimpleProduce(disableImmer);

const runActionReducerAtPath = (state, action, actionReducer, path) =>
simpleProduce(path, state, (draft) => actionReducer(draft, action.payload));
const runActionReducerAtPath = (state, action, actionReducer, path, config) =>
simpleProduce(path, state, (draft) => actionReducer(draft, action.payload), config);

const reducerForActions = (state, action) => {
const actionReducer = _aRD[action.type];
Expand All @@ -15,21 +15,22 @@ export default function createReducer(disableImmer, _aRD, _cR, _cP) {
action,
actionReducer,
actionReducer.def.meta.parent,
actionReducer.def.config,
);
}
return state;
};

const reducerForCustomReducers = (state, action) =>
_cR.reduce(
(acc, { parentPath, key, reducer }) =>
(acc, { parentPath, key, reducer, config }) =>
simpleProduce(parentPath, acc, (draft) => {
draft[key] = reducer(
isDraft(draft[key]) ? original(draft[key]) : draft[key],
action,
);
return draft;
}),
}, config),
state,
);

Expand Down
2 changes: 1 addition & 1 deletion src/extract-data-from-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default function extractDataFromModel(
bindComputedProperty(parent, _dS);
_cP.push({ key, parentPath, bindComputedProperty });
} else if (value[reducerSymbol]) {
_cR.push({ key, parentPath, reducer: value.fn });
_cR.push({ key, parentPath, reducer: value.fn, config: value.config });
} else if (value[effectOnSymbol]) {
const def = { ...value };

Expand Down
9 changes: 6 additions & 3 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ export const debug = (state) => {
return state;
};

export const actionOn = (targetResolver, fn) => ({
export const actionOn = (targetResolver, fn, config) => ({
[actionOnSymbol]: true,
fn,
targetResolver,
config
});

export const action = (fn) => ({
export const action = (fn, config) => ({
[actionSymbol]: true,
fn,
config
});

const defaultStateResolvers = [(state) => state];
Expand Down Expand Up @@ -77,7 +79,8 @@ export const thunk = (fn) => ({
fn,
});

export const reducer = (fn) => ({
export const reducer = (fn, config) => ({
[reducerSymbol]: true,
fn,
config
});
4 changes: 2 additions & 2 deletions src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export function set(path, target, value) {
}

export function createSimpleProduce(disableImmer = false) {
return function simpleProduce(path, state, fn) {
if (disableImmer) {
return function simpleProduce(path, state, fn, config) {
if ((config && 'immer' in config) ? config?.immer === false : disableImmer) {
const current = get(path, state);
const next = fn(current);
if (current !== next) {
Expand Down
54 changes: 54 additions & 0 deletions tests/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,57 @@ test('returning the state has no effect', () => {
// ASSERT
expect(store.getState()).toBe(prevState);
});

describe('disabling immer via actions config', () => {

test('not returning the state in action makes state undefined', () => {
// ARRANGE
const store = createStore({
count: 1,
addOne: action((state) => {
state.count += 1;
}, { immer: false }),
});

// ACT
store.getActions().addOne();

// ASSERT
expect(store.getState()).toBeUndefined();
});

test('returning the state in action works', () => {
// ARRANGE
const store = createStore({
count: 1,
addOne: action((state) => {
state.count += 1;
return state;
}, { immer: false }),
});

// ACT
store.getActions().addOne();

// ASSERT
expect(store.getState()).toEqual({count: 2});
});

test('explicitly enabling immer in action works without returning state', () => {
// ARRANGE
const store = createStore({
count: 1,
addOne: action((state) => {
state.count += 1;
}, { immer: true }),
});

// ACT
store.getActions().addOne();

// ASSERT
expect(store.getState()).toEqual({count: 2});
});

});

41 changes: 41 additions & 0 deletions tests/listener-actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,44 @@ it('thunk listening to multiple actions', async () => {
expect.anything(),
);
});

describe('disabling immer via configs', () => {
it('listening to an action, firing an action with immer disabled', () => {
// ARRANGE
const math = {
sum: 0,
add: action((state, payload) => {
state.sum += payload;
}),
};
const audit = {
logs: [],
onMathAdd: actionOn(
(_, storeActions) => storeActions.math.add,
(state, target) => {
expect(target.type).toBe('@action.math.add');
expect(target.payload).toBe(10);
expect(target.result).toBeUndefined();
expect(target.error).toBeUndefined();
expect(target.resolvedTargets).toEqual([target.type]);
state.logs.push(`Added ${target.payload}`);
},
{ immer: false },
),
};
const store = createStore({
math,
audit,
});

// ACT
store.getActions().math.add(10);

// ASSERT
expect(store.getState()).toEqual({
"math": {
"sum": 10
}
});
});
});
9 changes: 9 additions & 0 deletions website/docs/docs/api/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ An `action` is a function that is described below.

The payload, if any, that was provided to the
[action](/docs/api/action.html) when it was dispatched.

- `config` (Object)

The config object that was provided to the
[action](/docs/api/action.html) when it was dispatched.
- `immer` (Boolean)
Whether to use `immer` to update the state. Defaults to `true`.
s900mhz marked this conversation as resolved.
Show resolved Hide resolved

You may want to consider disabling immer when dealing with a large/deep data structure within your state. Immer does unfortunately have an overhead as it wraps data in a Proxy solution for the mutation based updates to work. We would suggest only using this escape hatch if you are noticing any performance degradation.

## Tutorial

Expand Down
5 changes: 5 additions & 0 deletions website/docs/docs/api/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ onAddTodo: actionOn(
// handler:
(state, target) => {
state.auditLog.push(`Added a todo: ${target.payload}`);
},
// optional config:
{
// whether to use immer to update the state. Defaults to true.
immer: true
}
)
```
Expand Down
8 changes: 8 additions & 0 deletions website/docs/docs/api/reducer.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ The `reducer` function is described below.

Any payload that was provided to the action.

- `config` (Object)

The config object that was provided to the action when it was dispatched.
- `immer` (Boolean)
Whether to use `immer` to update the state. Defaults to `true`.
s900mhz marked this conversation as resolved.
Show resolved Hide resolved

You may want to consider disabling immer when dealing with a large/deep data structure within your state. Immer does unfortunately have an overhead as it wraps data in a Proxy solution for the mutation based updates to work. We would suggest only using this escape hatch if you are noticing any performance degradation.

## Example

```javascript
Expand Down