Compare commits
4 Commits
e6c8a5106d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 44cac6da4d | |||
| d5e3f24497 | |||
| 9bfca72399 | |||
| 3979790c15 |
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
parser: 'babel-eslint',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 8,
|
ecmaVersion: 8,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "poi --prod",
|
"build": "poi --prod",
|
||||||
"start": "poi --serve",
|
"dev": "poi --serve",
|
||||||
|
"start": "npm run dev",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
@@ -13,11 +14,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"immer": "^2.1.3",
|
"immer": "^2.1.3",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.8.4",
|
"react": "^16.8.4",
|
||||||
"react-dom": "^16.8.4"
|
"react-dom": "^16.8.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@poi/plugin-eslint": "^12.0.0",
|
"@poi/plugin-eslint": "^12.0.0",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
"eslint": "^5.3.0",
|
"eslint": "^5.3.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-config-prettier": "^4.1.0",
|
"eslint-config-prettier": "^4.1.0",
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
/* eslint no-param-reassign: 0 */
|
/* eslint no-param-reassign: 0 */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TodoList from '../../components/TodoList';
|
import TodoList from '../../components/TodoList';
|
||||||
import initialState from '../../initial_state';
|
import { StateProvider, useStore } from './state/index';
|
||||||
import { todoState } from './state/todos';
|
import initTodos from './state/todos';
|
||||||
|
|
||||||
const TodoState = () => {
|
initTodos();
|
||||||
const [, actions] = todoState(initialState);
|
|
||||||
|
const TodoListWrapper = ({ children }) => {
|
||||||
|
const { state, commit } = useStore('todos');
|
||||||
|
const { todos } = state;
|
||||||
|
|
||||||
const onChange = action => {
|
const onChange = action => {
|
||||||
const { todo, completed, name } = action.payload;
|
const { todo, completed, name } = action.payload;
|
||||||
if (action.type === 'completed') actions.setCompleted(todo, completed);
|
const todoMatch = todos.find(t => t.id === todo.id);
|
||||||
if (action.type === 'rename') actions.setName(todo, name);
|
if (todoMatch < 0) return;
|
||||||
if (action.type === 'remove') actions.remove(todo);
|
|
||||||
|
const { id: todoId } = todoMatch;
|
||||||
|
|
||||||
|
if (action.type === 'completed') commit('markCompleted', todoId, completed);
|
||||||
|
if (action.type === 'rename' && name) commit('rename', todoId, name);
|
||||||
|
if (action.type === 'remove') commit('remove', todoId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// janky prop injection, just to reduce boilerplate here
|
||||||
|
return React.cloneElement(children, { onChange, todos });
|
||||||
|
};
|
||||||
|
|
||||||
|
const TodoState = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<StateProvider>
|
||||||
<h1>State Demo</h1>
|
<h1>State Demo</h1>
|
||||||
<TodoList onChange={onChange} />
|
{/* TodoListWrapper provides todos and onChange props to TodoList */}
|
||||||
|
<TodoListWrapper>
|
||||||
|
<TodoList />
|
||||||
|
</TodoListWrapper>
|
||||||
|
</StateProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
121
src/apps/todo-state/state/index.js
Normal file
121
src/apps/todo-state/state/index.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint no-param-reassign: 0, react/destructuring-assignment: 0, react/no-multi-comp: 0 */
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { produce, original } from 'immer';
|
||||||
|
|
||||||
|
// keep track of all the stores
|
||||||
|
const stores = new Map();
|
||||||
|
|
||||||
|
// USAGE: createStore('myStore', { state, mutations, actions, getters })
|
||||||
|
export function createStore(name, storeSpec) {
|
||||||
|
// don't duplicate stores
|
||||||
|
if (stores.has(name)) throw new Error(`Store '${name}' already exists`);
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
state: {},
|
||||||
|
mutations: {},
|
||||||
|
actions: {},
|
||||||
|
getters: {},
|
||||||
|
...storeSpec,
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a new context for the store and add it to the map
|
||||||
|
const storeContext = createContext({
|
||||||
|
state: spec.state,
|
||||||
|
commit: () => {},
|
||||||
|
dispatch: () => {},
|
||||||
|
getters: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
stores.set(name, {
|
||||||
|
context: storeContext,
|
||||||
|
provider: class StoreProvider extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
state: spec.state,
|
||||||
|
commit: (n, ...args) => {
|
||||||
|
const mutation = spec.mutations[n];
|
||||||
|
if (!mutation) throw new Error(`No such mutation: ${n}`);
|
||||||
|
this.setState(
|
||||||
|
produce(draft => {
|
||||||
|
mutation({ state: draft.state, original }, ...args);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
dispatch: async (n, ...args) => {
|
||||||
|
const action = spec.actions[n];
|
||||||
|
if (!action) throw new Error(`No such action: ${n}`);
|
||||||
|
return produce(this.state.state, async draft => {
|
||||||
|
// NOTE: this does not change state, it's expected that
|
||||||
|
// actions use commit to update the state
|
||||||
|
return action({ state: draft, commit: this.state.commit }, ...args);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get: (n, ...args) => {
|
||||||
|
const getter = spec.getters[n];
|
||||||
|
if (!getter) throw new Error(`No such getter: ${n}`);
|
||||||
|
return getter(this.state.state, ...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return React.createElement(
|
||||||
|
storeContext.Provider,
|
||||||
|
{ value: this.state },
|
||||||
|
this.props.children
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: keep track of the intial state, for resetting
|
||||||
|
// initialState: { ...spec.state },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: method to build a larger state object made of stores
|
||||||
|
// composeStores({
|
||||||
|
// todos: todoStore
|
||||||
|
// });
|
||||||
|
|
||||||
|
// hook to access a specific store and read/write methods
|
||||||
|
// USAGE: const { state, commit } = useStore('myStore')
|
||||||
|
export function useStore(name) {
|
||||||
|
if (!stores.has(name)) throw new Error(`Store does not exist: '${name}'`);
|
||||||
|
const store = stores.get(name);
|
||||||
|
return useContext(store.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// provider to wrap your app or component
|
||||||
|
export class StateProvider extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
names: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
names: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { names, children } = this.props;
|
||||||
|
const namesArray = names.length ? names.split(',') : [];
|
||||||
|
|
||||||
|
// provide all stores if none are specified
|
||||||
|
const provideAllStores = namesArray.length === 0;
|
||||||
|
|
||||||
|
// build an array of all the store providers
|
||||||
|
const providers = provideAllStores
|
||||||
|
? Array.from(stores).map(([, store]) => store.provider)
|
||||||
|
: namesArray.map(name => {
|
||||||
|
if (!stores.has(name)) throw new Error(`Store does not exist: '${name}'`);
|
||||||
|
return stores.get(name).provider;
|
||||||
|
});
|
||||||
|
|
||||||
|
// nest children inside all of the providers
|
||||||
|
return providers.reduce((acc, provider) => {
|
||||||
|
return React.createElement(provider, {}, acc);
|
||||||
|
}, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,59 @@
|
|||||||
/* eslint no-param-reassign: 0 */
|
/* eslint no-param-reassign: 0 */
|
||||||
import { useEffect } from 'react';
|
import { createStore } from '.';
|
||||||
import { useImmerReducer, original } from '../../../hooks/use_immer';
|
|
||||||
|
|
||||||
let currentState;
|
export default () =>
|
||||||
|
createStore('todos', {
|
||||||
export const setState = state => {
|
// initial state
|
||||||
currentState = state;
|
state: {
|
||||||
};
|
todos: [
|
||||||
|
{
|
||||||
export function todoState(initialState) {
|
id: 1,
|
||||||
if (!currentState && initialState) setState(initialState);
|
name: 'eat',
|
||||||
|
complete: false,
|
||||||
const [todos, dispatch] = useImmerReducer(
|
|
||||||
(state, action) => {
|
|
||||||
const { todo, completed, name } = action.payload;
|
|
||||||
const todoIndex = original(state).findIndex(t => t === todo);
|
|
||||||
if (todoIndex < 0) return;
|
|
||||||
|
|
||||||
if (action.type === 'setState') state = action.payload;
|
|
||||||
if (action.type === 'completed') state[todoIndex].completed = completed;
|
|
||||||
if (action.type === 'rename' && name) state[todoIndex].name = name;
|
|
||||||
if (action.type === 'remove') state.splice(todoIndex, 1);
|
|
||||||
},
|
},
|
||||||
currentState,
|
{
|
||||||
state => state || []
|
id: 2,
|
||||||
);
|
name: 'sleep',
|
||||||
|
completed: false,
|
||||||
useEffect(() => {
|
|
||||||
dispatch({ type: 'setState', currentState });
|
|
||||||
}, [currentState]);
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
setCompleted(todo, completed = true) {
|
|
||||||
dispatch({ type: 'completed', payload: { todo, completed } });
|
|
||||||
},
|
},
|
||||||
setName(todo, name) {
|
{
|
||||||
dispatch({ type: 'rename', payload: { todo, name } });
|
id: 3,
|
||||||
|
name: 'breath',
|
||||||
|
completed: false,
|
||||||
},
|
},
|
||||||
remove(todo) {
|
],
|
||||||
dispatch({ type: 'remove', payload: { todo } });
|
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
return [todos, actions];
|
// sync changes
|
||||||
}
|
mutations: {
|
||||||
|
add({ state }, name) {
|
||||||
|
state.todos.push({
|
||||||
|
name,
|
||||||
|
id: state.todos.length + 1,
|
||||||
|
completed: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove({ state, original }, id) {
|
||||||
|
state.todos = original(state).todos.filter(t => t.id !== id);
|
||||||
|
},
|
||||||
|
markCompleted({ state, original }, id, completed = true) {
|
||||||
|
const idx = original(state).todos.findIndex(t => t.id === id);
|
||||||
|
state.todos[idx].completed = completed;
|
||||||
|
},
|
||||||
|
rename({ state }, id, name) {
|
||||||
|
state.todos = state.todos.map(t => {
|
||||||
|
if (t.id === id) t.name = name;
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// async actions that cal mutations
|
||||||
|
actions: {},
|
||||||
|
|
||||||
|
// pull details out of state
|
||||||
|
getters: {
|
||||||
|
completedCount: state => state.todos.filter(t => t.completed).length,
|
||||||
|
outstandingCount: state => state.todos.filter(t => !t.completed).length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { todoState } from '../apps/todo-state/state/todos';
|
|
||||||
import TodoItem from './TodoItem';
|
import TodoItem from './TodoItem';
|
||||||
|
|
||||||
const TodoList = ({ onChange }) => {
|
const TodoList = ({ todos, onChange }) => {
|
||||||
const [todos] = todoState();
|
|
||||||
|
|
||||||
const onComplete = todo => () => {
|
const onComplete = todo => () => {
|
||||||
onChange({ type: 'completed', payload: { todo, completed: !todo.completed } });
|
onChange({ type: 'completed', payload: { todo, completed: !todo.completed } });
|
||||||
};
|
};
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@@ -233,7 +233,7 @@
|
|||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/parser@^7.2.2", "@babel/parser@^7.3.4":
|
"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4":
|
||||||
version "7.3.4"
|
version "7.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
|
||||||
integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
|
integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
|
||||||
@@ -708,7 +708,7 @@
|
|||||||
"@babel/parser" "^7.2.2"
|
"@babel/parser" "^7.2.2"
|
||||||
"@babel/types" "^7.2.2"
|
"@babel/types" "^7.2.2"
|
||||||
|
|
||||||
"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4":
|
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4":
|
||||||
version "7.3.4"
|
version "7.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06"
|
||||||
integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==
|
integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==
|
||||||
@@ -1230,6 +1230,18 @@ babel-code-frame@^6.26.0:
|
|||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
js-tokens "^3.0.2"
|
js-tokens "^3.0.2"
|
||||||
|
|
||||||
|
babel-eslint@^10.0.1:
|
||||||
|
version "10.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
|
||||||
|
integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.0.0"
|
||||||
|
"@babel/parser" "^7.0.0"
|
||||||
|
"@babel/traverse" "^7.0.0"
|
||||||
|
"@babel/types" "^7.0.0"
|
||||||
|
eslint-scope "3.7.1"
|
||||||
|
eslint-visitor-keys "^1.0.0"
|
||||||
|
|
||||||
babel-helper-vue-jsx-merge-props@^2.0.3:
|
babel-helper-vue-jsx-merge-props@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
|
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
|
||||||
@@ -2717,6 +2729,14 @@ eslint-rule-docs@^1.1.5:
|
|||||||
resolved "https://registry.yarnpkg.com/eslint-rule-docs/-/eslint-rule-docs-1.1.76.tgz#fafaaced271d28d0fdb5d8b8e0553abe8f755c24"
|
resolved "https://registry.yarnpkg.com/eslint-rule-docs/-/eslint-rule-docs-1.1.76.tgz#fafaaced271d28d0fdb5d8b8e0553abe8f755c24"
|
||||||
integrity sha512-8Aaae6mULkI7W2V530mexiMSMkqEI4uDL09JsNeXZ7Ei3nJ5eTnixWccMf3kEJa2IyfeHbPehLL3kgFTorABjg==
|
integrity sha512-8Aaae6mULkI7W2V530mexiMSMkqEI4uDL09JsNeXZ7Ei3nJ5eTnixWccMf3kEJa2IyfeHbPehLL3kgFTorABjg==
|
||||||
|
|
||||||
|
eslint-scope@3.7.1:
|
||||||
|
version "3.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
|
||||||
|
integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
|
||||||
|
dependencies:
|
||||||
|
esrecurse "^4.1.0"
|
||||||
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-scope@^4.0.0, eslint-scope@^4.0.2:
|
eslint-scope@^4.0.0, eslint-scope@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
|
||||||
@@ -5675,7 +5695,7 @@ promise-inflight@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||||
|
|
||||||
prop-types@^15.6.2:
|
prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user