Skip to content

Commit

Permalink
Merge pull request #21 from msmith-techempower/fix-state-closure-bug
Browse files Browse the repository at this point in the history
Update async API to fix state closure bug - 0.4.0
  • Loading branch information
NateBrady23 committed May 30, 2019
2 parents a42ecdb + 7517c02 commit 3c40374
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 348 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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


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

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

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

This should feel very similar to how `useReducer` works with actions and
reducers.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@techempower/react-governor",
"version": "0.3.0",
"version": "0.4.0",
"description": "The easiest way to govern over your application's state and actions.",
"license": "MIT",
"repository": {
Expand Down
28 changes: 0 additions & 28 deletions src/examples/class-counter/Counter.js

This file was deleted.

65 changes: 0 additions & 65 deletions src/examples/class-counter/CounterContract.js

This file was deleted.

19 changes: 11 additions & 8 deletions src/examples/counter/CounterContract.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const contract = {
count: state.count + val + val2
};
},
addNewState(val) {
addNewState(val, state) {
return {
...this.state,
...state,
newState: val
};
},
Expand All @@ -46,20 +46,23 @@ export const contract = {
resolve(256);
}, 1000)
);

// set the state count to our promised count
return {
count: count
};
this.set(count);
},
async fetchGoogle() {
let google = await fetch("https://www.google.com");

this.setStatus(google.status);
},
setStatus(status) {
return {
status: google.status
status
};
},
statedInc() {
statedInc(state) {
return {
count: this.state.count + 1
count: state.count + 1
};
}
};
16 changes: 8 additions & 8 deletions src/examples/simple-counter/SimpleCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import React from "react";
import { useGovernor } from "../..";

const contract = {
increment() {
return this.state + 1;
increment(state) {
return state + 1;
},
decrement() {
return this.state - 1;
decrement(state) {
return state - 1;
},
add(num) {
return this.state + num;
add(num, state) {
return state + num;
},
subtract(num) {
return this.state - num;
subtract(num, state) {
return state - num;
}
};

Expand Down
132 changes: 99 additions & 33 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,99 @@
import { useMemo, useReducer } from "react";

class HookActions {
constructor(contract, dispatch) {
this.__dispatch = dispatch;

if (typeof contract === "function") {
const _contract = new contract();
contract = {};
Object.getOwnPropertyNames(_contract.__proto__)
.splice(1) // remove the constructor
.forEach(key => (contract[key] = _contract[key]));
}

for (let key in contract) {
this[key] = async (...args) => {
const newState = await contract[key].apply({ state: this.__state }, [
...args,
this.__state
]);
this.__dispatch({
newState: newState
});
};
}
/**
* Helper function to take a contract and dispatch callback to transfrom
* them into an object of actions which ultimately dispatch to an underlying
* reducer.
*
* Example:
* const contract = {
* foo(bar, state) {
* return {
* ...state,
* bar
* };
* }
* };
*
* This contract will be turned into an action that is analogous to:
* {
* foo(bar) {
* dispatch({ "reduce": state => contract.foo(bar, state) });
* }
* }
*
*
* Async contract functions are a little different since they return a promise.
*
* Example:
* const contract = {
* async foo(bar, state) {
* return state => ({
* ...state,
* bar
* });
* }
* };
*
* This contract will be turned into an action that is analgous to:
* {
* foo(bar) {
* dispatch({ "reduce": state => state });
* contract.foo(bar, state).then(reducer => dispatch({
* "reduce": state => reducer(state)
* }));
* }
* }
*
* @param contract The contract from which actions are created
* @param dispatch The underlying useReducer's dispatch callback
*/
function createActions(contract, dispatch) {
const hookActions = {};

for (let key in contract) {
hookActions[key] = (...args) => {
dispatch({
reduce: state => {
const newState = contract[key](...args, state);

if (typeof newState.then === "undefined") {
// This was a non-async func; just return the new state
return newState;
}

newState.then(reducer => {
let error;
if (typeof reducer !== "function") {
error = new TypeError(
`async action "${key}" must return a reducer function; instead got "${typeof reducer}"`
);
}
// Once the promise is resolved, we need to dispatch a new
// action based on the reducer function the async func
// returns given the new state at the time of the resolution.
dispatch({
reduce: state => reducer(state),
error
});
});

// Async func cannot mutate state directly; return current state.
return state;
}
});
};
}

return hookActions;
}

// We do not inline this because it would cause 2 renders on first use.
function reducer(state, action) {
if (action.error) {
throw action.error;
}
return action.reduce(state);
}

/**
Expand All @@ -34,7 +104,7 @@ class HookActions {
* @returns [state, actions] - the current state of the governor and the
* actions that can be invoked.
*/
export function useGovernor(initialState = {}, contract = {}) {
function useGovernor(initialState = {}, contract = {}) {
if (
!contract ||
(typeof contract !== "object" && typeof contract !== "function")
Expand All @@ -45,15 +115,11 @@ export function useGovernor(initialState = {}, contract = {}) {
);
}

const [state, dispatch] = useReducer(
(state, action) => action.newState,
initialState
);
const [state, dispatch] = useReducer(reducer, initialState);

const hookActions = useMemo(() => new HookActions(contract, dispatch), [
contract
]);
hookActions.__state = state;
const actions = useMemo(() => createActions(contract, dispatch), [contract]);

return [state, hookActions];
return [state, actions];
}

export { useGovernor };
Loading

0 comments on commit 3c40374

Please sign in to comment.