Skip to content

Instantly share code, notes, and snippets.

@zaydek
Created August 30, 2021 20:36
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/a444f507f1a99b2c41c6ec824427bc5c to your computer and use it in GitHub Desktop.
Save zaydek/a444f507f1a99b2c41c6ec824427bc5c to your computer and use it in GitHub Desktop.
import create from "zustand"
import createContext from 'zustand/context'
import "./App.css"
////////////////////////////////////////////////////////////////////////////////
const LOCALSTORAGE_KEY = "zustand-todos"
function getStateLocalStorage() {
const jsonStr = window.localStorage.getItem(LOCALSTORAGE_KEY) // Zero value is `null`
return JSON.parse(jsonStr) // Zero value is `null`
}
function setStateLocalStorage(state) {
window.localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(state))
}
////////////////////////////////////////////////////////////////////////////////
const initialState = {
todo: {
checked: false,
value: "",
},
todos: [],
}
const types = {
editChecked: "editChecked",
editValue: "editValue",
commit: "commit",
editCheckedByIndex: "editCheckedByIndex",
editValueByIndex: "editValueByIndex",
deleteByIndex: "deleteByIndex",
}
function reducer(state, { type, data }) {
if (type === types.editChecked) {
return {
...state,
todo: {
...state.todo,
checked: data.checked,
},
}
} else if (type === types.editValue) {
return {
...state,
todo: {
...state.todo,
value: data.value,
},
}
} else if (type === types.commit) {
if (state.todo.value === "") {
return state
}
return {
...state,
todo: {
...state.todo,
value: "",
},
todos: [
{
id: Math.random().toString(36).slice(2, 6),
...state.todo,
},
...state.todos,
],
}
} else if (type === types.editCheckedByIndex) {
return {
...state,
todos: [
...state.todos.slice(0, data.todoIndex),
{
...state.todos[data.todoIndex],
checked: data.checked,
},
...state.todos.slice(data.todoIndex + 1),
]
}
} else if (type === types.editValueByIndex) {
return {
...state,
todos: [
...state.todos.slice(0, data.todoIndex),
{
...state.todos[data.todoIndex],
value: data.value,
},
...state.todos.slice(data.todoIndex + 1),
]
}
} else if (type === types.deleteByIndex) {
return {
...state,
todo: {
...state.todo,
value: "",
},
todos: [
...state.todos.slice(0, data.todoIndex),
...state.todos.slice(data.todoIndex + 1),
]
}
} else {
throw new Error("FIXME")
}
}
////////////////////////////////////////////////////////////////////////////////
function TodoForm() {
console.log("<TodoForm>")
const todo = useStore(state => state.todo)
const dispatch = useStore(state => state.dispatch)
return (
<form
onSubmit={e => {
e.preventDefault()
dispatch({
type: types.commit,
})
}}
>
<input
type="checkbox"
checked={todo.checked}
onChange={e => {
dispatch({
type: types.editChecked,
data: {
checked: e.target.checked,
},
})
}}
/>
<input
type="text"
value={todo.value}
onChange={e => {
dispatch({
type: types.editValue,
data: {
value: e.target.value,
},
})
}}
/>
<button type="submit">
+
</button>
</form>
)
}
// Note that this component is memoized because it's a direct child of an array
// and therefore nothing is preventing it from being rerendered when sibling
// elements are updated.
const MemoizedTodo = React.memo(function ({ todoIndex }) {
console.log(`<Todo todoIndex={${JSON.stringify(todoIndex)}}>`)
const todo = useStore(state => state.todos[todoIndex])
const dispatch = useStore(state => state.dispatch)
return (
<div id={todo.id}>
<input
type="checkbox"
checked={todo.checked}
onChange={e => {
dispatch({
type: types.editCheckedByIndex,
data: {
todoIndex,
checked: e.target.checked,
},
})
}}
/>
<input
type="text"
value={todo.value}
onChange={e => {
dispatch({
type: types.editValueByIndex,
data: {
todoIndex,
value: e.target.value,
},
})
}}
/>
<button
onClick={e => {
dispatch({
type: types.deleteByIndex,
data: {
todoIndex,
},
})
}}
>
-
</button>
</div>
)
})
function Todos() {
console.log("<Todos>")
const todos = useStore(state => state.todos)
return todos.map((todo, todoIndex) => <MemoizedTodo key={todo.id} todoIndex={todoIndex} />)
}
function Debug() {
const state = useStore(state => state)
return (
<pre style={{ fontSize: 14 }}>
{JSON.stringify({
state,
dispatch: undefined,
}, null, 2)}
</pre>
)
}
function LocalStorageSideEffect() {
console.log("<LocalStorageSideEffect>")
const state = useStore(state => ({ ...state, dispatch: undefined }))
React.useEffect(() => {
const id = setTimeout(() => {
setStateLocalStorage(state)
}, 250)
return () => {
clearTimeout(id)
}
}, [state])
return null
}
////////////////////////////////////////////////////////////////////////////////
const { Provider, useStore } = createContext()
function createStore() {
return create(set => ({
...initialState,
dispatch: args => set(state => reducer(state, args)),
}))
}
function createStoreFromLocalStorage() {
return create(set => ({
...(getStateLocalStorage() ?? initialState),
dispatch: args => set(state => reducer(state, args)),
}))
}
function App() {
console.log("<App>")
return (
<Provider createStore={createStoreFromLocalStorage}>
<TodoForm />
<Todos />
<LocalStorageSideEffect />
<Debug />
</Provider>
)
}
////////////////////////////////////////////////////////////////////////////////
export default function Top() {
return (
<>
<App />
{/* <App /> */}
</>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment