From e6c8a5106d0497046156700ac2c914b97edb1600 Mon Sep 17 00:00:00 2001 From: joe fleming Date: Fri, 15 Mar 2019 13:32:27 -0700 Subject: [PATCH] feat: a bunch of examples --- .eslintrc.js | 2 ++ babel.config.js | 5 +--- poi.config.js | 2 +- src/App.jsx | 39 +++++++++++++++++++++++++ src/apps/Home.jsx | 25 ++++++++++++++++ src/apps/immer-reducer/App.jsx | 28 ++++++++++++++++++ src/apps/immer/App.jsx | 47 ++++++++++++++++++++++++++++++ src/apps/todo-state/App.jsx | 25 ++++++++++++++++ src/apps/todo-state/state/todos.js | 46 +++++++++++++++++++++++++++++ src/apps/use-reducer/App.jsx | 44 ++++++++++++++++++++++++++++ src/apps/use-state/App.jsx | 44 ++++++++++++++++++++++++++++ src/assets/css/style.css | 34 +++++++++++++++++++++ src/components/TodoItem.jsx | 26 +++++++++++++++++ src/components/TodoList.jsx | 35 ++++++++++++++++++++++ src/hooks/use_immer.js | 19 ++++++++++++ src/index.js | 2 +- src/initial_state.js | 6 ++++ src/views/App.jsx | 5 ---- 18 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 src/App.jsx create mode 100644 src/apps/Home.jsx create mode 100644 src/apps/immer-reducer/App.jsx create mode 100644 src/apps/immer/App.jsx create mode 100644 src/apps/todo-state/App.jsx create mode 100644 src/apps/todo-state/state/todos.js create mode 100644 src/apps/use-reducer/App.jsx create mode 100644 src/apps/use-state/App.jsx create mode 100644 src/components/TodoItem.jsx create mode 100644 src/components/TodoList.jsx create mode 100644 src/hooks/use_immer.js create mode 100644 src/initial_state.js delete mode 100644 src/views/App.jsx diff --git a/.eslintrc.js b/.eslintrc.js index fea1f40..1be55c2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,5 +31,7 @@ module.exports = { 'peerDependencies': false } ], + 'react/jsx-one-expression-per-line': 'off', + 'react/prop-types': 'off', } } diff --git a/babel.config.js b/babel.config.js index 4c9f302..e4773fd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,8 +4,5 @@ module.exports = { // Our default preset 'poi/babel', ], - plugins: [ - // This adds Hot Reloading support - 'react-hot-loader/babel', - ], + plugins: [], }; diff --git a/poi.config.js b/poi.config.js index de26912..cba5034 100644 --- a/poi.config.js +++ b/poi.config.js @@ -1,5 +1,5 @@ /* eslint-env node */ module.exports = { entry: 'src/index', - plugins: ['@poi/plugin-eslint'], + // plugins: ['@poi/plugin-eslint'], }; diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..f208e17 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Home from './apps/Home'; +import UseState from './apps/use-state/App'; +import UseReducer from './apps/use-reducer/App'; +import Immer from './apps/immer/App'; +import ImmerReducer from './apps/immer-reducer/App'; +import TodoState from './apps/todo-state/App'; + +const { pathname } = window.location; + +const App = () => { + const getApp = () => { + switch (pathname) { + case '/use-state': + return ; + case '/use-reducer': + return ; + case '/immer': + return ; + case '/immer-reducer': + return ; + case '/todo-state': + return ; + default: + return ; + } + }; + + return ( +
+
+ Back Home +
+ {getApp()} +
+ ); +}; + +export default App(); diff --git a/src/apps/Home.jsx b/src/apps/Home.jsx new file mode 100644 index 0000000..42e4eb0 --- /dev/null +++ b/src/apps/Home.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const Home = () => ( + +); + +export default Home; diff --git a/src/apps/immer-reducer/App.jsx b/src/apps/immer-reducer/App.jsx new file mode 100644 index 0000000..2f99093 --- /dev/null +++ b/src/apps/immer-reducer/App.jsx @@ -0,0 +1,28 @@ +/* eslint no-param-reassign: 0 */ +import React from 'react'; +import initialState from '../../initial_state'; +import TodoList from '../../components/TodoList'; +import { useImmerReducer, original } from '../../hooks/use_immer'; + +const UseReducer = () => { + 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 === 'completed') state[todoIndex].completed = completed; + if (action.type === 'rename' && name) state[todoIndex].name = name; + if (action.type === 'remove') state.splice(todoIndex, 1); + }, initialState); + + const onChange = action => dispatch(action); + + return ( +
+

State Demo

+ +
+ ); +}; + +export default UseReducer; diff --git a/src/apps/immer/App.jsx b/src/apps/immer/App.jsx new file mode 100644 index 0000000..55e6419 --- /dev/null +++ b/src/apps/immer/App.jsx @@ -0,0 +1,47 @@ +/* eslint no-param-reassign: 0 */ +import React from 'react'; +import initialState from '../../initial_state'; +import TodoList from '../../components/TodoList'; +import { useImmer } from '../../hooks/use_immer'; + +const Immer = () => { + const [todos, setTodos] = useImmer(initialState); + + const onChange = action => { + const { todo, completed, name } = action.payload; + const todoIndex = todos.findIndex(t => t === todo); + if (todoIndex < 0) return; + + switch (action.type) { + case 'completed': { + setTodos(draft => { + draft[todoIndex].completed = completed; + }); + break; + } + case 'rename': { + if (name) + setTodos(draft => { + draft[todoIndex].name = name; + }); + break; + } + case 'remove': { + setTodos(draft => { + draft.splice(todoIndex, 1); + }); + break; + } + default: + } + }; + + return ( +
+

Immer Demo

+ +
+ ); +}; + +export default Immer; diff --git a/src/apps/todo-state/App.jsx b/src/apps/todo-state/App.jsx new file mode 100644 index 0000000..0656b82 --- /dev/null +++ b/src/apps/todo-state/App.jsx @@ -0,0 +1,25 @@ +/* eslint no-param-reassign: 0 */ +import React from 'react'; +import TodoList from '../../components/TodoList'; +import initialState from '../../initial_state'; +import { todoState } from './state/todos'; + +const TodoState = () => { + const [, actions] = todoState(initialState); + + const onChange = action => { + const { todo, completed, name } = action.payload; + if (action.type === 'completed') actions.setCompleted(todo, completed); + if (action.type === 'rename') actions.setName(todo, name); + if (action.type === 'remove') actions.remove(todo); + }; + + return ( +
+

State Demo

+ +
+ ); +}; + +export default TodoState; diff --git a/src/apps/todo-state/state/todos.js b/src/apps/todo-state/state/todos.js new file mode 100644 index 0000000..dd197d1 --- /dev/null +++ b/src/apps/todo-state/state/todos.js @@ -0,0 +1,46 @@ +/* eslint no-param-reassign: 0 */ +import { useEffect } from 'react'; +import { useImmerReducer, original } from '../../../hooks/use_immer'; + +let currentState; + +export const setState = state => { + currentState = state; +}; + +export function todoState(initialState) { + if (!currentState && initialState) setState(initialState); + + 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 || [] + ); + + 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 } }); + }, + remove(todo) { + dispatch({ type: 'remove', payload: { todo } }); + }, + }; + + return [todos, actions]; +} diff --git a/src/apps/use-reducer/App.jsx b/src/apps/use-reducer/App.jsx new file mode 100644 index 0000000..6b1aa21 --- /dev/null +++ b/src/apps/use-reducer/App.jsx @@ -0,0 +1,44 @@ +import React, { useReducer } from 'react'; +import initialState from '../../initial_state'; +import TodoList from '../../components/TodoList'; + +const UseReducer = () => { + const [todos, dispatch] = useReducer((state, action) => { + const { todo, completed, name } = action.payload; + const todoIndex = state.findIndex(t => t === todo); + if (todoIndex < 0) return state; + + switch (action.type) { + case 'completed': { + const newState = [...state]; + newState[todoIndex].completed = completed; + return newState; + } + case 'rename': { + if (!name) return state; + + const newState = [...state]; + newState[todoIndex].name = name; + return newState; + } + case 'remove': { + const newState = [...state]; + newState.splice(todoIndex, 1); + return newState; + } + default: + return state; + } + }, initialState); + + const onChange = action => dispatch(action); + + return ( +
+

State Demo

+ +
+ ); +}; + +export default UseReducer; diff --git a/src/apps/use-state/App.jsx b/src/apps/use-state/App.jsx new file mode 100644 index 0000000..ebbd0db --- /dev/null +++ b/src/apps/use-state/App.jsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import initialState from '../../initial_state'; +import TodoList from '../../components/TodoList'; + +const UseState = () => { + const [todos, setTodos] = useState(initialState); + + const onChange = action => { + const { todo, completed, name } = action.payload; + const todoIndex = todos.findIndex(t => t === todo); + if (todoIndex < 0) return; + + switch (action.type) { + case 'completed': { + const newState = [...todos]; + newState[todoIndex].completed = completed; + setTodos(newState); + break; + } + case 'rename': { + const newState = [...todos]; + newState[todoIndex].name = name; + setTodos(newState); + break; + } + case 'remove': { + const newState = [...todos]; + newState.splice(todoIndex, 1); + setTodos(newState); + break; + } + default: + } + }; + + return ( +
+

State Demo

+ +
+ ); +}; + +export default UseState; diff --git a/src/assets/css/style.css b/src/assets/css/style.css index e69de29..fe20c51 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -0,0 +1,34 @@ +body { + font-size: 16px; +} + +button { + border: 1px solid #666; + padding: 10px; + font-size: 1.2em; +} + +button.completed { + background-color: green; + color: white; +} + +button.edit { + color: blue; + font-weight: bold; + border: 0; +} + +button.remove { + color: red; + font-weight: bold; + border: 0; +} + +ul { + list-style: none; +} + +ul li { + margin: 4px 0; +} diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 0000000..2b36b25 --- /dev/null +++ b/src/components/TodoItem.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +const TodoItem = ({ todo, onComplete, onRename, onRemove }) => { + const className = todo.completed ? 'completed' : 'incomplete'; + + const editName = () => { + const name = window.prompt('Enter new name'); // eslint-disable-line no-alert + onRename(name); + }; + + return ( +
  • + + + +
  • + ); +}; + +export default TodoItem; diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 0000000..39675db --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { todoState } from '../apps/todo-state/state/todos'; +import TodoItem from './TodoItem'; + +const TodoList = ({ onChange }) => { + const [todos] = todoState(); + + const onComplete = todo => () => { + onChange({ type: 'completed', payload: { todo, completed: !todo.completed } }); + }; + + const onRemove = todo => () => { + onChange({ type: 'remove', payload: { todo } }); + }; + + const onRename = todo => name => { + onChange({ type: 'rename', payload: { todo, name } }); + }; + + return ( +
      + {todos.map(todo => ( + + ))} +
    + ); +}; + +export default TodoList; diff --git a/src/hooks/use_immer.js b/src/hooks/use_immer.js new file mode 100644 index 0000000..b38b5fd --- /dev/null +++ b/src/hooks/use_immer.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { produce } from 'immer'; + +// useful in useReducer since draft state is a proxy +export { original } from 'immer'; + +export function useImmer(initialValue) { + const [val, updateValue] = React.useState(initialValue); + return [ + val, + updater => { + updateValue(produce(updater)); + }, + ]; +} + +export function useImmerReducer(reducer, initialState, initialAction) { + return React.useReducer(produce(reducer), initialState, initialAction); +} diff --git a/src/index.js b/src/index.js index 58c9119..2632393 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom'; import './assets/css/style.css'; -import App from './views/App'; +import App from './App'; ReactDOM.render(App, document.getElementById('app')); diff --git a/src/initial_state.js b/src/initial_state.js new file mode 100644 index 0000000..53f4c65 --- /dev/null +++ b/src/initial_state.js @@ -0,0 +1,6 @@ +const items = ['eat', 'sleep', 'breath'].map(t => ({ + name: t, + complete: false, +})); + +export default items; diff --git a/src/views/App.jsx b/src/views/App.jsx deleted file mode 100644 index 18fd6ff..0000000 --- a/src/views/App.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const App = () =>
    Hello World!
    ; - -export default App();