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 (
+
+ );
+};
+
+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();