Skip to content

Instantly share code, notes, and snippets.

@zaydek
Last active August 29, 2021 04:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zaydek/43c61bbcffc7e099da52c70955e8fdaf to your computer and use it in GitHub Desktop.
Save zaydek/43c61bbcffc7e099da52c70955e8fdaf to your computer and use it in GitHub Desktop.
import "./App.css"
////////////////////////////////////////////////////////////////////////////////
// These are the possible action types our app can emit.
const TOGGLE_TODO = "TOGGLE_TODO"
const CHANGE_TODO = "CHANGE_TODO"
const COMMIT_TODO = "COMMIT_TODO"
const TOGGLE_TODO_BY_ID = "TOGGLE_TODO_BY_ID"
const CHANGE_TODO_BY_ID = "CHANGE_TODO_BY_ID"
const DELETE_TODO_BY_ID = "DELETE_TODO_BY_ID"
// This is the localStorage constant used to store and restore our app between
// sessions. This should generally never change.
const LOCALSTORAGE_KEY = "todos-app"
////////////////////////////////////////////////////////////////////////////////
// This describes an instance of a todo app.
const initialState = {
// This is the current todo.
todo: {
checked: false,
value: ""
},
// These are all the committed todos.
todos: [
// {
// id: <string>,
// checked: <boolean>,
// value: <string>,
// },
],
}
// Convenience function for generating four-character alphanumeric IDs that are
// supposed to be globally unique. Ideally you would use UUIDs here or similar.
function shortID() {
return Math.random().toString(36).slice(2, 6)
}
// This is a reducer; it describes how our apps state changes over time. If this
// looks terse you can always extract helper functions.
function todosReducer(state, { type, data }) {
// Given an action type, decide what to do. For example. if the action type is
// `TOGGLE_TODO`, we need to return the next state using functional syntax.
//
// The reason React expects you to write functional code is because of how
// React reconciles the DOM. In order to be fast, or fast enough, React uses
// shallow comparisons. Deep comparisons are functionally equivalent but more
// expensive.
//
// When you use functional syntax, primitive values such as booleans, number,
// and strings are copied. But reference types like arrays and objects are
// shallowly copied. This means React doesn't need to compare every element of
// an array or every property of an object to know whether your state changed.
if (type === TOGGLE_TODO) {
return {
...state,
todo: {
...state.todo,
checked: data.checked,
},
}
} else if (type === CHANGE_TODO) {
return {
...state,
todo: {
...state.todo,
value: data.value,
},
}
} else if (type === COMMIT_TODO) {
if (state.todo.value === "") {
return state
}
return {
...state,
todo: {
...initialState.todo,
},
todos: [
{
id: shortID(),
...state.todo,
},
...state.todos,
],
}
} else if (type === TOGGLE_TODO_BY_ID) {
const todoIndex = state.todos.findIndex(todo => todo.id === data.id)
return {
...state,
todos: [
...state.todos.slice(0, todoIndex),
{
...state.todos[todoIndex],
checked: data.checked,
},
...state.todos.slice(todoIndex + 1),
],
}
} else if (type === CHANGE_TODO_BY_ID) {
const todoIndex = state.todos.findIndex(todo => todo.id === data.id)
return {
...state,
todos: [
...state.todos.slice(0, todoIndex),
{
...state.todos[todoIndex],
value: data.value,
},
...state.todos.slice(todoIndex + 1),
],
}
} else if (type === DELETE_TODO_BY_ID) {
const todoIndex = state.todos.findIndex(todo => todo.id === data.id)
return {
...state,
todos: [
...state.todos.slice(0, todoIndex),
...state.todos.slice(todoIndex + 1),
],
}
} else {
throw new Error("FIXME")
}
}
////////////////////////////////////////////////////////////////////////////////
// Given an initial state, instantiate a todo app.
function useTodos(initialState) {
return React.useReducer(todosReducer, initialState)
}
// This is an adapter for our `useTodos` function. Instead, we want a todo app
// that is backed by localStorage that syncs every 100ms.
function useTodosLocalStorageAdapter() {
// If this is the first time a user is opening our app, use `initalState`.
// Otherwise, query the serialized state from localStorage.
const restoredState = React.useMemo(() => {
const serializedState = localStorage.getItem(LOCALSTORAGE_KEY)
if (serializedState !== null) {
return JSON.parse(serializedState)
}
return initialState
}, [])
// Instantiate our todos app using the restored state.
const [state, dispatch] = useTodos(restoredState)
// This is an effect; whenever our state changes, serialize and cache our
// state to localStorage. An effect is a side-effect that listens for
// dependencies.
//
// Dependencies are described as elements in an array, e.g. `[state]`. So
// whenever `state` changes, rerun this side-effect.
React.useEffect(() => {
const id = setTimeout(() => {
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(state))
}, 100)
// This is a cleanup function. In order to debounce localStorage writes,
// we clear the current timeout ID.
return () => {
clearTimeout(id)
}
}, [state])
// Return a tuple of our state and a dispatcher. A dispatcher is a function we
// use to invoke state changes.
return [state, dispatch]
}
////////////////////////////////////////////////////////////////////////////////
export default function App() {
const [state, dispatch] = useTodosLocalStorageAdapter()
return (
<div>
{/* Render the current todo. We can use a form in order to make use of
submit events. */}
<form
onSubmit={e => {
// `e.preventDefault` prevents the page from refreshing
e.preventDefault()
dispatch({
type: COMMIT_TODO,
})
}}
>
<input
type="checkbox"
checked={state.todo.checked}
onChange={e => {
dispatch({
type: TOGGLE_TODO,
data: {
checked: e.target.checked,
},
})
}}
/>
<input
type="text"
value={state.todo.value}
onChange={e => {
dispatch({
type: CHANGE_TODO,
data: {
value: e.target.value,
},
})
}}
/>
<button type="submit">
+
</button>
</form>
{/* For every todo, render a todo component. We don't have an extracted
`<Todo>` component; this is simply inlined. */}
<div>
{state.todos.map(todo => (
<div
key={todo.id}
id={todo.id}
>
<input
type="checkbox"
checked={todo.checked}
onChange={e => {
dispatch({
type: TOGGLE_TODO_BY_ID,
data: {
id: todo.id,
checked: e.target.checked,
},
})
}}
/>
<input
type="text"
value={todo.value}
onChange={e => {
dispatch({
type: CHANGE_TODO_BY_ID,
data: {
id: todo.id,
value: e.target.value,
},
})
}}
/>
<button
onClick={e => {
dispatch({
type: DELETE_TODO_BY_ID,
data: {
id: todo.id,
},
})
}}
>
-
</button>
</div>
))}
</div>
{/* This `<pre>` helps us debug our app state visually. In production this
would generally nto be visible to end users. */}
<pre
style={{ fontSize: 14 }}
>
{JSON.stringify(state, null, 2)}
</pre>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment