Skip to content

Commit 7685bcb

Browse files
committed
Adds the ability to declare dependencies between selectors
1 parent 67821ae commit 7685bcb

File tree

3 files changed

+120
-16
lines changed

3 files changed

+120
-16
lines changed

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,10 @@ Declares a section of state that is derived via the given selector function.
467467
468468
The local part of state that the `select` property was attached to.
469469
470+
- dependencies (Array, not required)
471+
472+
If this selector depends on other selectors your need to pass these selectors in here to indicate that is the case. Under the hood we will ensure the correct execution order.
473+
470474
Select's have their outputs cached to avoid unnecessary work, and will be executed
471475
any time their local state changes.
472476
@@ -483,10 +487,32 @@ const store = createStore({
483487
state.products.reduce((acc, cur) => acc + cur.price, 0)
484488
)
485489
}
486-
}
490+
};
487491

488492
// 👇 access the derived state as you would normal state
489-
store.getState().shoppingBasket.totalPrice
493+
store.getState().shoppingBasket.totalPrice;
494+
```
495+
496+
#### Example with Dependencies
497+
498+
```javascript
499+
import { select } from 'easy-peasy';
500+
501+
const totalPriceSelector = select(state =>
502+
state.products.reduce((acc, cur) => acc + cur.price, 0),
503+
)
504+
505+
const finalPriceSelector = select(
506+
state => state.totalPrice * ((100 - state.discount) / 100),
507+
[totalPriceSelector] // 👈 declare that this selector depends on totalPrice
508+
)
509+
510+
const store = createStore({
511+
discount: 25,
512+
products: [{ name: 'Shoes', price: 160 }, { name: 'Hat', price: 40 }],
513+
totalPrice: totalPriceSelector,
514+
finalPrice: finalPriceSelector
515+
});
490516
```
491517
492518
---

src/__tests__/easy-peasy.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,4 +476,62 @@ describe('select', () => {
476476
expect(actual).toEqual([{ text: 'foo' }, { text: 'bar' }])
477477
expect(selector).toHaveBeenCalledTimes(2)
478478
})
479+
480+
test('composed selectors', () => {
481+
// arrange
482+
const totalPriceSelector = select(state =>
483+
state.products.reduce((acc, cur) => acc + cur.price, 0),
484+
)
485+
const finalPriceSelector = select(
486+
state => state.totalPrice * ((100 - state.discount) / 100),
487+
[totalPriceSelector],
488+
)
489+
const store = createStore({
490+
discount: 25,
491+
products: [{ name: 'Shoes', price: 160 }, { name: 'Hat', price: 40 }],
492+
totalPrice: totalPriceSelector,
493+
finalPrice: finalPriceSelector,
494+
addProduct: (state, payload) => {
495+
state.products.push(payload)
496+
},
497+
changeDiscount: (state, payload) => {
498+
state.discount = payload
499+
},
500+
})
501+
502+
// assert
503+
expect(store.getState().finalPrice).toBe(150)
504+
505+
// act
506+
store.dispatch.addProduct({ name: 'Socks', price: 100 })
507+
508+
// assert
509+
expect(store.getState().finalPrice).toBe(225)
510+
511+
// act
512+
store.dispatch.changeDiscount(50)
513+
514+
// assert
515+
expect(store.getState().finalPrice).toBe(150)
516+
})
517+
518+
test('composed selectors in reverse decleration', () => {
519+
// arrange
520+
const totalPriceSelector = select(state =>
521+
state.products.reduce((acc, cur) => acc + cur.price, 0),
522+
)
523+
const finalPriceSelector = select(
524+
state => state.totalPrice * ((100 - state.discount) / 100),
525+
[totalPriceSelector],
526+
)
527+
const store = createStore({
528+
discount: 25,
529+
products: [{ name: 'Shoes', price: 160 }, { name: 'Hat', price: 40 }],
530+
finalPrice: finalPriceSelector,
531+
totalPrice: totalPriceSelector,
532+
})
533+
534+
// assert
535+
expect(store.getState().finalPrice).toBe(150)
536+
})
479537
})

src/easy-peasy.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import thunk from 'redux-thunk'
99

1010
const effectSymbol = Symbol('effect')
1111
const selectSymbol = Symbol('select')
12+
const selectDependeciesSymbol = Symbol('selectDependencies')
13+
const selectStateSymbol = Symbol('selectState')
1214

1315
const isObject = x => x && typeof x === 'object' && !Array.isArray(x)
1416

@@ -32,9 +34,11 @@ export const effect = fn => {
3234
return fn
3335
}
3436

35-
export const select = fn => {
37+
export const select = (fn, dependencies) => {
3638
const selector = memoizeOne(state => fn(state))
3739
selector[selectSymbol] = true
40+
selector[selectDependeciesSymbol] = dependencies
41+
selector[selectStateSymbol] = {}
3842
return selector
3943
}
4044

@@ -65,7 +69,8 @@ export const createStore = (model, options = {}) => {
6569

6670
if (value[selectSymbol]) {
6771
// skip
68-
selectorReducers.push([parentPath, key, value])
72+
value[selectStateSymbol] = { parentPath, key, executed: false }
73+
selectorReducers.push(value)
6974
} else if (value[effectSymbol]) {
7075
// Effect Action
7176
const action = payload => {
@@ -156,23 +161,38 @@ export const createStore = (model, options = {}) => {
156161
return state
157162
}
158163
let isInitial = true
164+
const runSelectorReducer = (state, selector) => {
165+
const { parentPath, key, executed } = selector[selectStateSymbol]
166+
if (executed) {
167+
return state
168+
}
169+
const dependencies = selector[selectDependeciesSymbol]
170+
const newState = produce(
171+
dependencies ? dependencies.reduce(runSelectorReducer, state) : state,
172+
draft => {
173+
// eslint-disable-next-line no-param-reassign
174+
const target = parentPath.length > 0 ? get(parentPath, draft) : draft
175+
if (target) {
176+
target[key] = selector(target)
177+
}
178+
},
179+
)
180+
// eslint-disable-next-line no-param-reassign
181+
selector[selectStateSymbol].executed = true
182+
return newState
183+
}
184+
const runSelectors = state =>
185+
selectorReducers.reduce(runSelectorReducer, state)
159186
return selectorReducers.length
160187
? (state, action) => {
161188
const stateAfterActions = reducerForActions(state, action)
162189
if (state !== stateAfterActions || isInitial) {
163-
const stateAfterSelectors = selectorReducers.reduce(
164-
(acc, [parentPath, key, selector]) =>
165-
produce(acc, draft => {
166-
// eslint-disable-next-line no-param-reassign
167-
const target =
168-
parentPath.length > 0 ? get(parentPath, draft) : draft
169-
if (target) {
170-
target[key] = selector(target)
171-
}
172-
}),
173-
stateAfterActions,
174-
)
190+
const stateAfterSelectors = runSelectors(stateAfterActions)
175191
isInitial = false
192+
selectorReducers.forEach(selector => {
193+
// eslint-disable-next-line no-param-reassign
194+
selector[selectStateSymbol].executed = false
195+
})
176196
return stateAfterSelectors
177197
}
178198
return stateAfterActions

0 commit comments

Comments
 (0)