Skip to content

Commit 3c40374

Browse files
authored
Merge pull request #21 from msmith-techempower/fix-state-closure-bug
Update async API to fix state closure bug - 0.4.0
2 parents a42ecdb + 7517c02 commit 3c40374

File tree

9 files changed

+152
-348
lines changed

9 files changed

+152
-348
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
![Build Status](https://travis-ci.org/TechEmpower/react-governor.svg?branch=master)
44

5-
65
Use a governor hook to manage state with actions for, and created by, the people.
76

87
Available as an [npm package](https://www.npmjs.com/package/@techempower/react-governor).
@@ -45,7 +44,7 @@ export default function Counter() {
4544
}
4645
```
4746

48-
[Test that this works](https://codesandbox.io/s/934jnrrpmr)
47+
[Test that this works](https://codesandbox.io/s/hopeful-shannon-lz433)
4948

5049
This should feel very similar to how `useReducer` works with actions and
5150
reducers.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@techempower/react-governor",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "The easiest way to govern over your application's state and actions.",
55
"license": "MIT",
66
"repository": {

src/examples/class-counter/Counter.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/examples/class-counter/CounterContract.js

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/examples/counter/CounterContract.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export const contract = {
3030
count: state.count + val + val2
3131
};
3232
},
33-
addNewState(val) {
33+
addNewState(val, state) {
3434
return {
35-
...this.state,
35+
...state,
3636
newState: val
3737
};
3838
},
@@ -46,20 +46,23 @@ export const contract = {
4646
resolve(256);
4747
}, 1000)
4848
);
49+
4950
// set the state count to our promised count
50-
return {
51-
count: count
52-
};
51+
this.set(count);
5352
},
5453
async fetchGoogle() {
5554
let google = await fetch("https://www.google.com");
55+
56+
this.setStatus(google.status);
57+
},
58+
setStatus(status) {
5659
return {
57-
status: google.status
60+
status
5861
};
5962
},
60-
statedInc() {
63+
statedInc(state) {
6164
return {
62-
count: this.state.count + 1
65+
count: state.count + 1
6366
};
6467
}
6568
};

src/examples/simple-counter/SimpleCounter.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import React from "react";
22
import { useGovernor } from "../..";
33

44
const contract = {
5-
increment() {
6-
return this.state + 1;
5+
increment(state) {
6+
return state + 1;
77
},
8-
decrement() {
9-
return this.state - 1;
8+
decrement(state) {
9+
return state - 1;
1010
},
11-
add(num) {
12-
return this.state + num;
11+
add(num, state) {
12+
return state + num;
1313
},
14-
subtract(num) {
15-
return this.state - num;
14+
subtract(num, state) {
15+
return state - num;
1616
}
1717
};
1818

src/index.js

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,99 @@
11
import { useMemo, useReducer } from "react";
22

3-
class HookActions {
4-
constructor(contract, dispatch) {
5-
this.__dispatch = dispatch;
6-
7-
if (typeof contract === "function") {
8-
const _contract = new contract();
9-
contract = {};
10-
Object.getOwnPropertyNames(_contract.__proto__)
11-
.splice(1) // remove the constructor
12-
.forEach(key => (contract[key] = _contract[key]));
13-
}
14-
15-
for (let key in contract) {
16-
this[key] = async (...args) => {
17-
const newState = await contract[key].apply({ state: this.__state }, [
18-
...args,
19-
this.__state
20-
]);
21-
this.__dispatch({
22-
newState: newState
23-
});
24-
};
25-
}
3+
/**
4+
* Helper function to take a contract and dispatch callback to transfrom
5+
* them into an object of actions which ultimately dispatch to an underlying
6+
* reducer.
7+
*
8+
* Example:
9+
* const contract = {
10+
* foo(bar, state) {
11+
* return {
12+
* ...state,
13+
* bar
14+
* };
15+
* }
16+
* };
17+
*
18+
* This contract will be turned into an action that is analogous to:
19+
* {
20+
* foo(bar) {
21+
* dispatch({ "reduce": state => contract.foo(bar, state) });
22+
* }
23+
* }
24+
*
25+
*
26+
* Async contract functions are a little different since they return a promise.
27+
*
28+
* Example:
29+
* const contract = {
30+
* async foo(bar, state) {
31+
* return state => ({
32+
* ...state,
33+
* bar
34+
* });
35+
* }
36+
* };
37+
*
38+
* This contract will be turned into an action that is analgous to:
39+
* {
40+
* foo(bar) {
41+
* dispatch({ "reduce": state => state });
42+
* contract.foo(bar, state).then(reducer => dispatch({
43+
* "reduce": state => reducer(state)
44+
* }));
45+
* }
46+
* }
47+
*
48+
* @param contract The contract from which actions are created
49+
* @param dispatch The underlying useReducer's dispatch callback
50+
*/
51+
function createActions(contract, dispatch) {
52+
const hookActions = {};
53+
54+
for (let key in contract) {
55+
hookActions[key] = (...args) => {
56+
dispatch({
57+
reduce: state => {
58+
const newState = contract[key](...args, state);
59+
60+
if (typeof newState.then === "undefined") {
61+
// This was a non-async func; just return the new state
62+
return newState;
63+
}
64+
65+
newState.then(reducer => {
66+
let error;
67+
if (typeof reducer !== "function") {
68+
error = new TypeError(
69+
`async action "${key}" must return a reducer function; instead got "${typeof reducer}"`
70+
);
71+
}
72+
// Once the promise is resolved, we need to dispatch a new
73+
// action based on the reducer function the async func
74+
// returns given the new state at the time of the resolution.
75+
dispatch({
76+
reduce: state => reducer(state),
77+
error
78+
});
79+
});
80+
81+
// Async func cannot mutate state directly; return current state.
82+
return state;
83+
}
84+
});
85+
};
2686
}
87+
88+
return hookActions;
89+
}
90+
91+
// We do not inline this because it would cause 2 renders on first use.
92+
function reducer(state, action) {
93+
if (action.error) {
94+
throw action.error;
95+
}
96+
return action.reduce(state);
2797
}
2898

2999
/**
@@ -34,7 +104,7 @@ class HookActions {
34104
* @returns [state, actions] - the current state of the governor and the
35105
* actions that can be invoked.
36106
*/
37-
export function useGovernor(initialState = {}, contract = {}) {
107+
function useGovernor(initialState = {}, contract = {}) {
38108
if (
39109
!contract ||
40110
(typeof contract !== "object" && typeof contract !== "function")
@@ -45,15 +115,11 @@ export function useGovernor(initialState = {}, contract = {}) {
45115
);
46116
}
47117

48-
const [state, dispatch] = useReducer(
49-
(state, action) => action.newState,
50-
initialState
51-
);
118+
const [state, dispatch] = useReducer(reducer, initialState);
52119

53-
const hookActions = useMemo(() => new HookActions(contract, dispatch), [
54-
contract
55-
]);
56-
hookActions.__state = state;
120+
const actions = useMemo(() => createActions(contract, dispatch), [contract]);
57121

58-
return [state, hookActions];
122+
return [state, actions];
59123
}
124+
125+
export { useGovernor };

0 commit comments

Comments
 (0)