Last active
August 29, 2021 04:41
-
-
Save zaydek/43c61bbcffc7e099da52c70955e8fdaf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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