From ba302f76e72213776fbbb0bb7965f125a7aa7989 Mon Sep 17 00:00:00 2001 From: Yad Smood <1415488+ysmood@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:08:49 +0800 Subject: [PATCH] modulize the TodoApp --- README.md | 5 +- examples/TodoApp/Actions.tsx | 35 ++++++++ examples/TodoApp/Filter.tsx | 12 +++ examples/TodoApp/Title.tsx | 5 ++ examples/TodoApp/TodoItem.tsx | 24 ++++++ examples/TodoApp/TodoList.tsx | 13 +++ examples/TodoApp/ToggleAll.tsx | 14 +++ examples/TodoApp/index.tsx | 94 ++------------------- examples/TodoApp/store.ts | 136 ------------------------------ examples/TodoApp/todos/actions.ts | 42 +++++++++ examples/TodoApp/todos/filter.ts | 13 +++ examples/TodoApp/todos/index.ts | 86 +++++++++++++++++++ examples/index.tsx | 20 ++++- examples/utils.ts | 4 + examples/utils.tsx | 14 --- vitest.config.ts | 1 + 16 files changed, 274 insertions(+), 244 deletions(-) create mode 100644 examples/TodoApp/Actions.tsx create mode 100644 examples/TodoApp/Filter.tsx create mode 100644 examples/TodoApp/Title.tsx create mode 100644 examples/TodoApp/TodoItem.tsx create mode 100644 examples/TodoApp/TodoList.tsx create mode 100644 examples/TodoApp/ToggleAll.tsx delete mode 100644 examples/TodoApp/store.ts create mode 100644 examples/TodoApp/todos/actions.ts create mode 100644 examples/TodoApp/todos/filter.ts create mode 100644 examples/TodoApp/todos/index.ts create mode 100644 examples/utils.ts delete mode 100644 examples/utils.tsx diff --git a/README.md b/README.md index 3c6705a..d9d356c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An elegant state management solution for React. - **Non-opinionated**: Like useState, only one core function, others are built on top of it. - **Type safe**: The state is type safe and the return value is intuitive. - **Global**: The state is global, you can access it anywhere. -- **Scalable**: Supports selector to avoid unnecessary re-render. +- **Scalable**: Naturally scale large state into multiple modules and files without performance degradation. - **Tiny**: Less than [0.3KB](https://bundlephobia.com/package/create-global-state). ## Documentation @@ -44,4 +44,5 @@ Its implementation is not type safe and the return value is not intuitive. It's > Why not [zustand](https://github.com/pmndrs/zustand)? -The typescript support is not good enough, the API is not intuitive. `create-global-state` is more like `useState` which aligns with the react API style. Check the [comparison](https://github.com/ysmood/create-global-state/issues/1). +The typescript support is not good enough, the API is not intuitive. `create-global-state` is more like `useState` which aligns with the react API style. Check the [comparison](https://github.com/ysmood/create-global-state/issues/1). Zustand [Slices Pattern](https://zustand.docs.pmnd.rs/guides/slices-pattern) can cause naming conflict issues. +`create-global-state` can naturally scale states by modules and files. diff --git a/examples/TodoApp/Actions.tsx b/examples/TodoApp/Actions.tsx new file mode 100644 index 0000000..4f3262e --- /dev/null +++ b/examples/TodoApp/Actions.tsx @@ -0,0 +1,35 @@ +import Filter from "./Filter"; +import { addTodo, clearCompleted } from "./todos/actions"; +import { useZeroDone } from "./todos"; +import ToggleAll from "./ToggleAll"; + +export default function Actions() { + return ( +
+ + + + +
+ ); +} + +function AddTodo() { + return ( + + ); +} + +function ClearCompleted() { + return ( + + ); +} diff --git a/examples/TodoApp/Filter.tsx b/examples/TodoApp/Filter.tsx new file mode 100644 index 0000000..a80ad3c --- /dev/null +++ b/examples/TodoApp/Filter.tsx @@ -0,0 +1,12 @@ +import { filters, setFilter } from "./todos/filter"; + +// The component to filter the todos. +export default function Filter() { + return ( + + ); +} diff --git a/examples/TodoApp/Title.tsx b/examples/TodoApp/Title.tsx new file mode 100644 index 0000000..6e754a4 --- /dev/null +++ b/examples/TodoApp/Title.tsx @@ -0,0 +1,5 @@ +import { useLeftCount } from "./todos"; + +export default function Title() { + return

Todo App ({useLeftCount()} todos left)

; +} diff --git a/examples/TodoApp/TodoItem.tsx b/examples/TodoApp/TodoItem.tsx new file mode 100644 index 0000000..452fedd --- /dev/null +++ b/examples/TodoApp/TodoItem.tsx @@ -0,0 +1,24 @@ +import { delTodo, toggleTodo, updateTodo, useTodo } from "./todos"; + +// The component to display a todo. +export default function TodoItem({ id }: { id: number }) { + const { done, text } = useTodo(id); + + return ( +
+ toggleTodo(id)} /> + + updateTodo(id, e.target.value)} + disabled={done} + autoFocus + /> + + +
+ ); +} diff --git a/examples/TodoApp/TodoList.tsx b/examples/TodoApp/TodoList.tsx new file mode 100644 index 0000000..e3a2369 --- /dev/null +++ b/examples/TodoApp/TodoList.tsx @@ -0,0 +1,13 @@ +import { useTodoIds } from "./todos"; +import TodoItem from "./TodoItem"; + +// The component to display all filtered todos. +export default function Todos() { + return ( + <> + {useTodoIds().map((id) => { + return ; + })} + + ); +} diff --git a/examples/TodoApp/ToggleAll.tsx b/examples/TodoApp/ToggleAll.tsx new file mode 100644 index 0000000..55c0309 --- /dev/null +++ b/examples/TodoApp/ToggleAll.tsx @@ -0,0 +1,14 @@ +import { toggleAll, useToggleAll } from "./todos/actions"; +import { useZeroCount } from "./todos"; + +// The component to toggle all todos. +export default function ToggleAll() { + return ( + + ); +} diff --git a/examples/TodoApp/index.tsx b/examples/TodoApp/index.tsx index 9db05d9..cbfd85f 100644 --- a/examples/TodoApp/index.tsx +++ b/examples/TodoApp/index.tsx @@ -1,95 +1,13 @@ -import { - useList, - useTodo, - delTodo, - toggleTodo, - updateTodo, - addTodo, - clearCompleted, - filters, - setFilter, - toggleAll, - useCount, - useToggleAll, -} from "./store"; +import Actions from "./Actions"; +import Title from "./Title"; +import TodoList from "./TodoList"; export default function TodoApp() { - return ( -
-

Todo App ({useCount(false)} todos left)

-
- - - - -
- - -
- ); -} - -// The component to toggle all todos. -function ToggleAll() { - return ( - - ); -} - -// The component to filter the todos. -function Filter() { - return ( - - ); -} - -// The component to display all filtered todos. -function TodoList() { return ( <> - {useList().map((id) => { - return ; - })} + + <Actions /> + <TodoList /> </> ); } - -// The component to display a todo. -function TodoItem({ id }: { id: number }) { - const { done, text } = useTodo(id); - - return ( - <div className="flex gap-1 my-1"> - <input type="checkbox" checked={done} onChange={() => toggleTodo(id)} /> - - <input - placeholder="Input text here" - value={text} - onChange={(e) => updateTodo(id, e.target.value)} - disabled={done} - autoFocus - /> - - <button onClick={() => delTodo(id)} title="Delete current todo"> - ✕ - </button> - </div> - ); -} diff --git a/examples/TodoApp/store.ts b/examples/TodoApp/store.ts deleted file mode 100644 index e19b0df..0000000 --- a/examples/TodoApp/store.ts +++ /dev/null @@ -1,136 +0,0 @@ -import create from "create-global-state/lib/immer"; -import { useEqual } from "create-global-state"; - -export const filters = ["All", "Active", "Completed"] as const; - -export type Filter = (typeof filters)[number]; - -// Define type and init value of the store -const initStore = { - todos: [ - { - id: Date.now(), - text: "", - done: false, - }, - ], - filter: "All" as Filter, -}; - -// Create a store, it uses the url hash to persist the state, the key in the url hash is "todo-app". -const [useStore, setStore] = create(initStore); - -// Add a new empty todo to the list. -export function addTodo() { - setStore((s) => { - s.todos.unshift({ ...initStore.todos[0], id: Date.now() }); - }); -} - -// Get a new id list only when the ids of the todos change. -export function useList() { - return useStore( - useEqual((s) => { - return filterTodos(s).map(({ id }) => id); - }, numbersEqual) - ); -} - -// Get the count of the left todos. -export function useCount(done?: boolean) { - return useStore((s) => s.todos.filter(({ done: d }) => d == done).length); -} - -// Get a todo by id. -export function useTodo(id: number) { - return useStore((s) => findTodo(s, id)); -} - -// Get the toggleAll state. -export function useToggleAll() { - return useStore((s) => { - const todos = filterTodos(s); - if (todos.length === 0) { - return false; - } - return todos.every(({ done }) => done); - }); -} - -// Delete a todo by id. -export function delTodo(id: number) { - setStore((s) => { - s.todos = s.todos.filter((todo) => todo.id !== id); - }); -} - -// Toggle the done state of a todo by id. -export function toggleTodo(id: number) { - setStore((s) => { - const todo = findTodo(s, id); - if (todo) { - todo.done = !todo.done; - } - }); -} - -// Toggle all the current filtered todos. -export function toggleAll() { - setStore((s) => { - const todos = filterTodos(s); - if (todos.every(({ done }) => done)) { - todos.forEach((todo) => { - todo.done = false; - }); - } else { - todos.forEach((todo) => { - todo.done = true; - }); - } - }); -} - -// Update the text of a todo by id. -export function updateTodo(id: number, text: string) { - setStore((s) => { - findTodo(s, id).text = text; - }); -} - -// Clear all completed todos. -export function clearCompleted() { - setStore((s) => { - s.todos = s.todos.filter(({ done }) => !done); - }); -} - -// Set the filter for filtering the todos. -export function setFilter(filter: string) { - setStore((s) => { - s.filter = filter as Filter; - }); -} - -// Find a todo by id. -function findTodo(s: typeof initStore, id: number) { - return s.todos.find((todo) => todo.id === id)!; -} - -// Filter the todos by the current status. -function filterTodos(s: typeof initStore) { - return s.todos.filter(({ done }) => { - switch (s.filter) { - case "All": - return true; - case "Active": - return !done; - case "Completed": - return done; - } - }); -} - -// Compare two arrays' equality. -function numbersEqual<T>(x: T[], y: T[]) { - return x.length === y.length && x.every((v, i) => v === y[i]); -} diff --git a/examples/TodoApp/todos/actions.ts b/examples/TodoApp/todos/actions.ts new file mode 100644 index 0000000..5bb6c97 --- /dev/null +++ b/examples/TodoApp/todos/actions.ts @@ -0,0 +1,42 @@ +import { getFilter, useFilter } from "./filter"; +import { filterTodos, initTodo, setTodos, useTodos } from "."; + +// Add a new empty todo to the list. +export function addTodo() { + setTodos((todos) => { + todos.unshift({ ...initTodo, id: Date.now() }); + }); +} + +// Clear all completed todos. +export function clearCompleted() { + setTodos((todos) => todos.filter(({ done }) => !done)); +} + +// Get the toggleAll state. +export function useToggleAll() { + const filter = useFilter(); + return useTodos((todos) => { + todos = filterTodos(todos, filter); + if (todos.length === 0) { + return false; + } + return todos.every(({ done }) => done); + }); +} + +// Toggle all the current filtered todos. +export function toggleAll() { + setTodos((todos) => { + todos = filterTodos(todos, getFilter()); + if (todos.every(({ done }) => done)) { + todos.forEach((todo) => { + todo.done = false; + }); + } else { + todos.forEach((todo) => { + todo.done = true; + }); + } + }); +} diff --git a/examples/TodoApp/todos/filter.ts b/examples/TodoApp/todos/filter.ts new file mode 100644 index 0000000..e0d022d --- /dev/null +++ b/examples/TodoApp/todos/filter.ts @@ -0,0 +1,13 @@ +import create from "create-global-state"; + +const initFilter = "All"; + +export const filters = [initFilter, "Active", "Completed"] as const; + +export type Filter = (typeof filters)[number]; + +export const [useFilter, setFilterBase, getFilter] = create<Filter>(initFilter); + +export const setFilter = (filter: string) => { + setFilterBase(filter as Filter); +}; diff --git a/examples/TodoApp/todos/index.ts b/examples/TodoApp/todos/index.ts new file mode 100644 index 0000000..3622122 --- /dev/null +++ b/examples/TodoApp/todos/index.ts @@ -0,0 +1,86 @@ +import create from "create-global-state/lib/immer"; +import { useEqual } from "create-global-state"; +import { type Filter, useFilter } from "./filter"; +import { numbersEqual } from "../../utils"; + +export const initTodo = { + id: Date.now(), + text: "", + done: false, +}; + +export const [useTodos, setTodos] = create([initTodo]); + +type Todos = (typeof initTodo)[]; + +// Get a todo by id. +export function useTodo(id: number) { + return useTodos((s) => findTodo(s, id)); +} + +// Get a new id list only when the ids of the todos change. +export function useTodoIds() { + const filter = useFilter(); + return useTodos( + useEqual((todos) => { + return filterTodos(todos, filter).map(({ id }) => id); + }, numbersEqual) + ); +} + +export function useZeroCount() { + const filter = useFilter(); + return useTodos((todos) => { + return filterTodos(todos, filter).map(({ id }) => id).length === 0; + }); +} + +// Get the count of the left todos. +export function useLeftCount() { + return useTodos((todos) => todos.filter(({ done }) => !done).length); +} + +export function useZeroDone() { + return useTodos((todos) => !todos.some(({ done }) => done)); +} + +// Update the text of a todo by id. +export function updateTodo(id: number, text: string) { + setTodos((todos) => { + findTodo(todos, id).text = text; + }); +} + +// Toggle the done state of a todo by id. +export function toggleTodo(id: number) { + setTodos((todos) => { + const todo = findTodo(todos, id); + if (todo) { + todo.done = !todo.done; + } + }); +} + +// Delete a todo by id. +export function delTodo(id: number) { + setTodos((todos) => todos.filter((todo) => todo.id !== id)); +} + +// Find a todo by id. +function findTodo(todos: Todos, id: number) { + return todos.find((todo) => todo.id === id)!; +} + +// Filter the todos by the current status. +export function filterTodos(todos: Todos, filter: Filter) { + return todos.filter(({ done }) => { + switch (filter) { + case "All": + return true; + case "Active": + return !done; + case "Completed": + return done; + } + }); +} diff --git a/examples/index.tsx b/examples/index.tsx index c078392..4f8cefb 100644 --- a/examples/index.tsx +++ b/examples/index.tsx @@ -1,12 +1,12 @@ import "./index.css"; import ReactDOM from "react-dom/client"; -import { Link, Switch, Router } from "wouter"; -import { ExampleRoute } from "./utils"; +import { Link, Switch, Router, Route } from "wouter"; +import { lazy, StrictMode, Suspense } from "react"; const examples = ["Counter", "CounterPersistent", "MonolithStore", "TodoApp"]; ReactDOM.createRoot(document.getElementById("root")!).render( - <> + <StrictMode> {examples.map((name) => { return ( <Link href={`/examples/${name}`} key={name} className={"mx-1"}> @@ -23,5 +23,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render( ))} </Router> </Switch> - </> + </StrictMode> ); + +export function ExampleRoute({ name, path }: { name: string; path?: string }) { + path = path || `/${name}`; + const Example = lazy(() => import(/* @vite-ignore */ `./${name}`)); + return ( + <Route path={path}> + <Suspense fallback={<div>Loading...</div>}> + <Example /> + </Suspense> + </Route> + ); +} diff --git a/examples/utils.ts b/examples/utils.ts new file mode 100644 index 0000000..d214665 --- /dev/null +++ b/examples/utils.ts @@ -0,0 +1,4 @@ +// Compare two arrays' equality. +export function numbersEqual<T>(x: T[], y: T[]) { + return x.length === y.length && x.every((v, i) => v === y[i]); +} diff --git a/examples/utils.tsx b/examples/utils.tsx deleted file mode 100644 index 4c98426..0000000 --- a/examples/utils.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { lazy, Suspense } from "react"; -import { Route } from "wouter"; - -export function ExampleRoute({ name, path }: { name: string; path?: string }) { - path = path || `/${name}`; - const Example = lazy(() => import(/* @vite-ignore */ `./${name}`)); - return ( - <Route path={path}> - <Suspense fallback={<div>Loading...</div>}> - <Example /> - </Suspense> - </Route> - ); -} diff --git a/vitest.config.ts b/vitest.config.ts index 08c180a..152d043 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ enabled: true, name: "chromium", provider: "playwright", + headless: true, }, coverage: { provider: "istanbul",