feat: bunch of examples and state library

This commit is contained in:
2019-03-22 09:48:01 -07:00
parent 3979790c15
commit 9bfca72399
4 changed files with 202 additions and 53 deletions

View File

@@ -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>
<h1>State Demo</h1> <StateProvider>
<TodoList onChange={onChange} /> <h1>State Demo</h1>
{/* TodoListWrapper provides todos and onChange props to TodoList */}
<TodoListWrapper>
<TodoList />
</TodoListWrapper>
</StateProvider>
</div> </div>
); );
}; };

View 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);
}
}

View File

@@ -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; id: 2,
const todoIndex = original(state).findIndex(t => t === todo); name: 'sleep',
if (todoIndex < 0) return; completed: false,
},
if (action.type === 'setState') state = action.payload; {
if (action.type === 'completed') state[todoIndex].completed = completed; id: 3,
if (action.type === 'rename' && name) state[todoIndex].name = name; name: 'breath',
if (action.type === 'remove') state.splice(todoIndex, 1); completed: false,
},
],
}, },
currentState,
state => state || []
);
useEffect(() => { // sync changes
dispatch({ type: 'setState', currentState }); mutations: {
}, [currentState]); add({ state }, name) {
state.todos.push({
name,
id: state.todos.length,
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;
});
},
},
const actions = { // async actions that cal mutations
setCompleted(todo, completed = true) { actions: {},
dispatch({ type: 'completed', payload: { todo, completed } });
},
setName(todo, name) {
dispatch({ type: 'rename', payload: { todo, name } });
},
remove(todo) {
dispatch({ type: 'remove', payload: { todo } });
},
};
return [todos, 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,
},
});

View File

@@ -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 } });
}; };