Skip to content

Commit 0c14e91

Browse files
committed
feat: throws errors when arguments are invalid or state type is not a plain object
1 parent d528168 commit 0c14e91

File tree

3 files changed

+151
-95
lines changed

3 files changed

+151
-95
lines changed

README.md

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ The CDN puts the library on `window.CoreFlux`.
6060

6161
The one and only export of Core Flux. Use it to create a store instance. You can create as few or as many stores as your heart desires! They will all be independent from one another.
6262

63-
The function **requires** all four of its arguments, as shown here:
63+
_**NOTE**: The base state type must be a plain object. Invalid types will throw an error._
64+
65+
The function **requires** all four of its arguments, including [bindings](#bindings):
6466

6567
```js
6668
// foo-store.js
@@ -85,28 +87,6 @@ export { subscribe, dispatch }
8587

8688
Once a store is created, you'll be able to add subscriptions with `subscribe` and request state updates with `dispatch`.
8789

88-
#### Bindings
89-
90-
Here's a breakdown of each binding needed when initializing a new store:
91-
92-
**`reducer(state, action)`**
93-
94-
> `state (object)`: A _copy_ of the current state object.<br/>`action ({ type: string, payload: object })`: The dispatched action type and its payload.
95-
96-
Creates a new version of state and returns it, based on the `type` and `payload`. If the return value is falsy, the update process ends.
97-
98-
**`bindSubscriber(newSubscription, state)`**
99-
100-
> `newSubscription ([subscriber, data])`: A tuple containing the subscribed object and its state-relational data.<br/>`state (object)`: A _copy_ of the current state object.
101-
102-
Called after a new `subscribe` call is made and a subscription has been added to the store. Use it to set initial state on the new subscriber based on the `data` defined by your subscriber.
103-
104-
**`bindState(subscriptions, reducedState, setState)`**
105-
106-
> `subscriptions (subscription[])`: An array containing all subscriptions.<br/>`reducedState (object)`: The state object as returned by the reducer.<br/>`setState (function)`:
107-
108-
Called after the reducer has processed the next state value. Use it to set the reduced state back to subscribers **and** back to the store.
109-
11090
<h3 id="subscribe"><code>subscribe(subscriber, data)</code></h3>
11191

11292
Adds a subscription to your store. It will always be tied to a single store, and subsequently state object.
@@ -129,7 +109,7 @@ In the above example, we've designed the subscriber, the `FooItems` class, to de
129109

130110
After the subscribe call is made, your `bindSubscriber` function will be called where you can pass along the default values as you see fit.
131111

132-
> In general, you should try to use a simple data structure as the second argument to `subscribe`; this ensures your bindings have generic and consistent expectations.
112+
_**NOTE:** In general, you should try to use a simple data structure as the second argument to `subscribe`; this ensures your bindings have generic and consistent expectations._
133113

134114
<h3 id="dispatch"><code>dispatch(type, payload)</code></h3>
135115

@@ -164,11 +144,33 @@ The reducer could have a logic branch on the action type called `ADD_ITEM` which
164144

165145
Finally, the result would then be handed over to your `bindState` binding.
166146

167-
> Much like in `subscribe`, it's best to maintain data types in the payload so your reducer can have consistent expectations.
147+
_**NOTE:** Much like in `subscribe`, it's best to maintain data types in the payload so your reducer can have consistent expectations._
148+
149+
#### Bindings
150+
151+
Here's a breakdown of each binding needed when initializing a new store:
152+
153+
##### **`bindSubscriber(subscription, state)`**
154+
155+
> `subscription ([subscriber, data])`: A tuple containing the subscribed object and its state-relational data.<br/>`state (object)`: The current state object.
156+
157+
Called after a new `subscribe` is made and a subscription has been added to the store. Use it to _set initial state_ on the new subscriber. Use the `data` provided to infer a new operation, e.g., setting a stateful property to the subscriber.
158+
159+
##### **`reducer(state, action)`**
160+
161+
> `state (object)`: Snapshot of the current state object.<br/>`action ({ type: string, payload: object })`: The dispatched action type and its payload.
162+
163+
Called during a new `dispatch`. Create a new version of state and return it.
164+
165+
##### **`bindState(subscriptions, reducedState, setState)`**
166+
167+
> `subscriptions (subscription[])`: An array containing all subscriptions.<br/>`reducedState (object)`: The state object as returned by the reducer.<br/>`setState (function)`:
168+
169+
Called at the end of a `dispatch` call, after your reducer callback has processed the next state value. Set your new state back to subscribers **and** back to the store. It's possible and expected for you to call `bindSubscriber` again to DRYly apply these updates. You can return from this function safely to noop.
168170

169171
## Exposing the store
170172

171-
For utility or debugging reasons, you may want to look at the store you're working with. To do so, you can use the `__data` property when creating a store.
173+
For utility or debugging reasons, you may want to look at the store you're working with. To do so, you can use the `__data` property when creating a store:
172174

173175
```js
174176
const fooStore = createStore(initialState, reducer, bindSubscriber, bindState)
@@ -178,9 +180,11 @@ window.fooStoreData = fooStore.__data
178180
console.log(window.fooStoreData) // { state: {...}, subscriptions: [...] }
179181
```
180182

183+
_**NOTE:** Avoid including `__data` in production environments; the data is mutable and therefore exposes a security risk if accessible._
184+
181185
## Data model
182186

183-
Core Flux has a relatively simple data model that you should understand when creating your bindings.
187+
Core Flux has a relatively simple data model that you should understand when creating [bindings](#bindings).
184188

185189
Here is how state looks in all cases:
186190

@@ -198,7 +202,7 @@ Store {
198202

199203
Each item in `subscriptions` contains a `subscriber` and some form of `data` that informs a relationship between `state` and `subscriber`.
200204

201-
NOTE: You define `data` in the above model, be it an object, array, string; it can be anything you want. Ultimately, you're responsible for communicating state relationships to subscribers.
205+
_**NOTE:** \_You_ define `data` in the above model. This ensures that ultimately you control communicating state relationships to subscribers.\_
202206

203207
## Data flow
204208

__tests__/core-flux.spec.js

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ const mockReducer = jest.fn()
66

77
const testSubscriberData = "variable test data"
88
const testSubscriber = {}
9+
const TEST_TYPE = "test"
10+
const FAIL_TYPE = "fail"
911

1012
function testReducer(state, action) {
11-
if (action.type === "TEST_TYPE") {
12-
state.foo = action.payload.foo
13-
return state
13+
switch (action.type) {
14+
case TEST_TYPE: {
15+
state.foo = action.payload.foo
16+
return state
17+
}
18+
case FAIL_TYPE: {
19+
return action.payload
20+
}
1421
}
1522
}
1623

@@ -23,7 +30,7 @@ function getMockStore() {
2330
}
2431

2532
describe("createStore", () => {
26-
describe("artifacts", () => {
33+
describe("initialize", () => {
2734
it("returns dispatch and subscribe helper functions", () => {
2835
// Given
2936
const Store = getMockStore()
@@ -43,10 +50,24 @@ describe("createStore", () => {
4350
expect.objectContaining({ state: {}, subscriptions: [] })
4451
)
4552
})
53+
54+
it("throws error if invalid initialState", () => {
55+
// Given
56+
const badStates = [false, null, undefined, [], 123, "foo"]
57+
58+
badStates.forEach((badState) => {
59+
// When
60+
const createBadStore = () => createStore(badState, mockReducer)
61+
62+
// Then
63+
expect(createBadStore).toThrow(
64+
"[core-flux] createStore(): The initial state value must be a plain object."
65+
)
66+
})
67+
})
4668
})
4769

4870
describe("state bindings", () => {
49-
const TEST_TYPE = "TEST_TYPE"
5071
const testPayload = { foo: "bar" }
5172

5273
it("calls reducer on dispatch", () => {
@@ -57,7 +78,7 @@ describe("createStore", () => {
5778
Store.dispatch(TEST_TYPE, testPayload)
5879

5980
// Then
60-
expect(mockReducer).toBeCalledWith(
81+
expect(mockReducer).toHaveBeenCalledWith(
6182
Store.__data.state,
6283
expect.objectContaining({ payload: testPayload, type: TEST_TYPE })
6384
)
@@ -77,7 +98,7 @@ describe("createStore", () => {
7798
Store.dispatch(TEST_TYPE, testPayload)
7899

79100
// Then
80-
expect(mockBindState).toBeCalledWith(
101+
expect(mockBindState).toHaveBeenCalledWith(
81102
Store.__data.subscriptions,
82103
expect.objectContaining({ foo: "bar" }),
83104
expect.any(Function)
@@ -101,6 +122,27 @@ describe("createStore", () => {
101122
expect.objectContaining({ foo: "bar" })
102123
)
103124
})
125+
126+
it("throws error if next state value is not a plain object", () => {
127+
// Given
128+
const Store = createStore(
129+
{},
130+
testReducer,
131+
mockBindSubscriber,
132+
testBindState
133+
)
134+
const badStates = [false, null, undefined, [], 123, "foo"]
135+
136+
badStates.forEach((badState) => {
137+
// When
138+
const dispatchBadState = () => Store.dispatch(FAIL_TYPE, badState)
139+
140+
// Then
141+
expect(dispatchBadState).toThrow(
142+
"[core-flux] bindState callback: The reduced state value must be a plain object. If there is no change in state, simply return it."
143+
)
144+
})
145+
})
104146
})
105147

106148
describe("subscriber bindings", () => {
@@ -112,7 +154,7 @@ describe("createStore", () => {
112154
Store.subscribe(testSubscriber, testSubscriberData)
113155

114156
// Then
115-
expect(mockBindSubscriber).toBeCalledWith(
157+
expect(mockBindSubscriber).toHaveBeenCalledWith(
116158
Store.__data.subscriptions[0],
117159
Store.__data.state
118160
)
@@ -132,5 +174,27 @@ describe("createStore", () => {
132174
])
133175
)
134176
})
177+
178+
it("throws error if invalid subscriber", () => {
179+
// Given
180+
const Store = getMockStore()
181+
const subscribe = () => Store.subscribe(null, testSubscriberData)
182+
183+
// Then
184+
expect(subscribe).toThrow(
185+
"[core-flux] subscribe(): `subscriber` and `data` arguments are required."
186+
)
187+
})
188+
189+
it("throws error if invalid subscriber data", () => {
190+
// Given
191+
const Store = getMockStore()
192+
const subscribe = () => Store.subscribe(testSubscriber, null)
193+
194+
// Then
195+
expect(subscribe).toThrow(
196+
"[core-flux] subscribe(): `subscriber` and `data` arguments are required."
197+
)
198+
})
135199
})
136200
})

0 commit comments

Comments
 (0)