TodoMVC in React
Just a practise of react18, and it’s a demo for my friend.
It includes
- TodoMVC template
- React18 for browser
- useReducer
- createContext
- useEffect
- etc
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css" />
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css" />
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css" />
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
/>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<!--<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Taste JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web" />
</li>-->
</ul>
</section>
<!-- This footer should be hidden by default and shown when there are todos -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count"><strong>0</strong> item left</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script>
<script type="text/babel" src="js/app.js"></script>
</body>
</html>
app.js
(function (window) {
"use strict";
// ------------------------------------------------------------
// State
// ------------------------------------------------------------
// reducer for better state managing
const types = Object.freeze({
ACTION_TODO_ADDED: "ACTION_TODO_ADDED",
ACTION_TODO_UPDATED: "ACTION_TODO_UPDATED",
ACTION_TODO_DELETED: "ACTION_TODO_DELETED",
ACTION_TODO_CLEAR_COMPLETED: "ACTION_TODO_CLEAR_COMPLETED",
ACTION_TODO_TOGGLE_ALL: "ACTION_TODO_TOGGLE_ALL",
ACTION_FILTER_CHANGED: "ACTION_FILTER_CHANGED",
});
const initialState = {
todos: JSON.parse(localStorage.getItem("todos")) || [],
filter: "#/",
};
function todosReducer(todos, action) {
if (action.type === types.ACTION_TODO_ADDED) {
return [
...todos,
{
id: Date.now(),
title: action.title,
completed: false,
},
];
} else if (action.type === types.ACTION_TODO_UPDATED) {
return todos.map((todo) => {
if (todo.id === action.todo.id) {
return action.todo;
} else {
return todo;
}
});
} else if (action.type === types.ACTION_TODO_DELETED) {
return todos.filter((todo) => todo.id !== action.id);
} else if (action.type === types.ACTION_TODO_CLEAR_COMPLETED) {
return todos.filter((todo) => !todo.completed);
} else if (action.type === types.ACTION_TODO_TOGGLE_ALL) {
return todos.map((todo) => {
todo.completed = action.completed;
return todo;
});
} else {
return todos;
}
}
function filterReducer(filter, action) {
if (types.ACTION_FILTER_CHANGED === action.type) {
return action.filter;
} else {
return filter;
}
}
function stateReducer(state, action) {
const newState = {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action),
};
if (state.todos !== newState.todos) {
localStorage.setItem("todos", JSON.stringify(newState.todos));
}
return newState;
}
// context for sharing reducer between components
const StateContext = React.createContext(null);
const StateDispatchContext = React.createContext(null);
function StateProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<StateContext.Provider value={state}>
<StateDispatchContext.Provider value={dispatch}>
{children}
</StateDispatchContext.Provider>
</StateContext.Provider>
);
}
function useStateContext() {
return React.useContext(StateContext);
}
function useStateDispatchContext() {
return React.useContext(StateDispatchContext);
}
// ------------------------------------------------------------
// Components
// ------------------------------------------------------------
function Header() {
const dispatch = useStateDispatchContext();
function handleKeyPress(e) {
if (e.key === "Enter") {
dispatch({
type: types.ACTION_TODO_ADDED,
title: e.target.value,
});
e.target.value = "";
}
}
return (
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
onKeyPress={handleKeyPress}
/>
</header>
);
}
function Todo({ todo }) {
const dispatch = useStateDispatchContext();
const [editing, setEditing] = React.useState(false);
const inputRef = React.useRef(null);
React.useEffect(() => {
if (editing) {
inputRef.current.value = todo.title;
inputRef.current.focus();
}
}, [editing]);
const className = [];
if (todo.completed) {
className.push("completed");
}
if (editing) {
className.push("editing");
}
return (
<li key={todo.id} className={className.join(" ")}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={(e) => {
dispatch({
type: types.ACTION_TODO_UPDATED,
todo: {
...todo,
completed: e.target.checked,
},
});
}}
/>
<label
onClick={(e) => {
if (e.detail === 2) {
setEditing(true);
}
}}
>
{todo.title}
</label>
<button
className="destroy"
onClick={() => {
dispatch({
type: types.ACTION_TODO_DELETED,
id: todo.id,
});
}}
></button>
</div>
<input
ref={inputRef}
className="edit"
defaultValue={todo.title}
onKeyPress={function (e) {
if (e.key === "Enter") {
setEditing(false);
}
}}
onBlur={(e) => {
setEditing(false);
dispatch({
type: types.ACTION_TODO_UPDATED,
todo: {
...todo,
title: e.target.value,
},
});
}}
/>
</li>
);
}
function Main() {
const { todos, filter } = useStateContext();
const dispatch = useStateDispatchContext();
const isAllCompleted = !todos.find((todo) => !todo.completed);
return (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
checked={isAllCompleted}
onChange={(e) => {
dispatch({
type: types.ACTION_TODO_TOGGLE_ALL,
completed: e.target.checked,
});
}}
/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{todos
.filter((todo) => {
if (filter === "#/") {
return true;
} else if (filter === "#/active") {
return !todo.completed;
} else if (filter === "#/completed") {
return todo.completed;
}
})
.map((todo) => (
<Todo key={todo.id} todo={todo} />
))}
</ul>
</section>
);
}
function Footer() {
const { todos, filter } = useStateContext();
const dispatch = useStateDispatchContext();
const count = todos.filter((todo) => !todo.completed).length;
React.useEffect(() => {
dispatch({
type: types.ACTION_FILTER_CHANGED,
filter: location.hash,
});
window.addEventListener("hashchange", (e) => {
dispatch({
type: types.ACTION_FILTER_CHANGED,
filter: location.hash,
});
});
}, []);
function filterClassName(filterValue) {
return filterValue === filter ? "selected" : "";
}
return (
<footer className="footer">
<span className="todo-count">
<strong>{count}</strong> item left
</span>
<ul className="filters">
<li>
<a className={filterClassName("#/")} href="#/">
All
</a>
</li>
<li>
<a className={filterClassName("#/active")} href="#/active">
Active
</a>
</li>
<li>
<a className={filterClassName("#/completed")} href="#/completed">
Completed
</a>
</li>
</ul>
<button
className="clear-completed"
onClick={() => {
dispatch({
type: types.ACTION_TODO_CLEAR_COMPLETED,
});
}}
>
Clear completed
</button>
</footer>
);
}
function TodoApp() {
return (
<StateProvider>
<Header />
<Main />
<Footer />
</StateProvider>
);
}
const root = ReactDOM.createRoot(document.querySelector(".todoapp"));
root.render(<TodoApp />);
})(window);