Skip to content

Commit

Permalink
refactor to utils module and middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmood committed Oct 18, 2024
1 parent 7ffce61 commit dd1d808
Show file tree
Hide file tree
Showing 33 changed files with 501 additions and 316 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-console': ['error', { allow: ['info', 'warn', 'error'] }],
},
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Overview

An elegant state management solution for React.
The philosophy of this project is to keep the core simple and scalable by exposing low-level accessibility and middleware composition, not by adding options.
All the non-core functions are just examples of how you can compose functions to achieve common features.

## Features

- **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**: Naturally scale large state into multiple modules and files without performance degradation.
- **Tiny**: Less than [0.3KB](https://bundlephobia.com/package/create-global-state).
- **Middlewares**: Simple and type-safe middleware composition interface.
- **Tiny**: About [0.3KB](https://bundlephobia.com/package/create-global-state) Minified + Gzipped.

## Documentation

Expand Down Expand Up @@ -46,3 +49,4 @@ Its implementation is not type safe and the return value is not intuitive. It's
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.
The setter of zustand must be called within a react component, while the setter of `create-global-state` can be called anywhere.
13 changes: 13 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"words": [
"gettify",
"immer",
"immerify",
"subscribify",
"todomvc",
"todos",
"tsbuildinfo",
"wouter",
"zustand"
]
}
8 changes: 6 additions & 2 deletions examples/CounterPersistent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import create from "create-global-state/lib/persistent";
import create, { saveHistory } from "create-global-state/lib/persistent";
import { useEffect } from "react";
import { useHashLocation } from "wouter/use-hash-location";

Expand All @@ -22,7 +22,11 @@ export default function CounterPersistent() {
}

function Button() {
return <button onClick={() => setCount((c) => c + 1, true)}>Increase</button>;
return (
<button onClick={() => setCount((c) => c + 1, { [saveHistory]: true })}>
Increase
</button>
);
}

function Display() {
Expand Down
2 changes: 1 addition & 1 deletion examples/MonolithStore/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Here we use Immer to update the store immutably, Immer is optional,
// you can use vanilla js or other libs to update the store.
import create from "create-global-state/lib/immer";
import { create } from "create-global-state/lib/immer";

// Define the monolith store for the entire web app.
// It contains a list of counters and a title string.
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoApp/Actions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Filter from "./Filter";
import { addTodo, clearCompleted } from "./todos/actions";
import { useZeroDone } from "./todos";
import { addTodo, clearCompleted } from "./store/actions";
import { useZeroDone } from "./store/todos";
import ToggleAll from "./ToggleAll";

export default function Actions() {
Expand Down
6 changes: 5 additions & 1 deletion examples/TodoApp/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { filters, setFilter } from "./todos/filter";
import { useEffect } from "react";
import { setFilter } from "./store/filter";
import { filters } from "./store/filter/types";

// The component to filter the todos.
export default function Filter() {
useEffect(() => {});

return (
<select onChange={(e) => setFilter(e.target.value)}>
{filters.map((f) => (
Expand Down
14 changes: 14 additions & 0 deletions examples/TodoApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Overview

An example of how to use `create-global-state` to build a scalable todo app.
The example is intentionally split the app into tiny modules to show how to scale the app.
So that when you update part of the state, only the minimal components will re-render, not the whole todo list or app.
Compare the re-rendering with todomvc.com via [react chrome extension](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), you will see the difference:

`create-global-state`:

![create-global-state](https://github.com/user-attachments/assets/dc405ff6-fe6e-40f5-8703-e0a2e1aa7b8e)

[todomvc.com](https://todomvc.com/examples/react/dist/):

![todomvc](https://github.com/user-attachments/assets/9e175ab1-0a16-4819-a079-c3ffb4238695)
2 changes: 1 addition & 1 deletion examples/TodoApp/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLeftCount } from "./todos";
import { useLeftCount } from "./store/todos";

export default function Title() {
return <h3>Todo App ({useLeftCount()} todos left)</h3>;
Expand Down
48 changes: 35 additions & 13 deletions examples/TodoApp/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { delTodo, toggleTodo, updateTodo, useTodo } from "./todos";
import {
delTodo,
toggleTodo,
updateTodo,
useTodo,
useTodoDone,
} from "./store/todos";

// The component to display a todo.
export default function TodoItem({ id }: { id: number }) {
const { done, text } = useTodo(id);
type AttrID = { id: number };

// The component to display a todo.
export default function TodoItem({ id }: AttrID) {
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
/>
<Done id={id} />
<Input id={id} />

<button onClick={() => delTodo(id)} title="Delete current todo">
</button>
</div>
);
}

function Done({ id }: AttrID) {
return (
<input
type="checkbox"
checked={useTodoDone(id)}
onChange={() => toggleTodo(id)}
/>
);
}

function Input({ id }: AttrID) {
const { done, text } = useTodo(id);
return (
<input
placeholder="Input text here"
value={text}
onChange={(e) => updateTodo(id, e.target.value)}
disabled={done}
autoFocus
/>
);
}
2 changes: 1 addition & 1 deletion examples/TodoApp/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useTodoIds } from "./todos";
import { useTodoIds } from "./store/todos";
import TodoItem from "./TodoItem";

// The component to display all filtered todos.
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoApp/ToggleAll.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toggleAll, useToggleAll } from "./todos/actions";
import { useZeroCount } from "./todos";
import { toggleAll, useToggleAll } from "./store/actions";
import { useZeroCount } from "./store/todos";

// The component to toggle all todos.
export default function ToggleAll() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { getFilter, useFilter } from "./filter";
import { filterTodos, initTodo, setTodos, useTodos } from ".";
import { setStore, useStore } from ".";
import { initTodo } from "./todos/types";
import { filterTodos } from "./todos";

// Add a new empty todo to the list.
export function addTodo() {
setTodos((todos) => {
todos.unshift({ ...initTodo, id: Date.now() });
setStore((s) => {
s.todos.unshift({ ...initTodo, id: Date.now() });
});
}

// Clear all completed todos.
export function clearCompleted() {
setTodos((todos) => todos.filter(({ done }) => !done));
setStore((s) => {
s.todos = s.todos.filter(({ done }) => !done);
});
}

// Get the toggleAll state.
export function useToggleAll() {
const filter = useFilter();
return useTodos((todos) => {
todos = filterTodos(todos, filter);
return useStore((s) => {
const todos = filterTodos(s, s.filter);
if (todos.length === 0) {
return false;
}
Expand All @@ -27,8 +29,8 @@ export function useToggleAll() {

// Toggle all the current filtered todos.
export function toggleAll() {
setTodos((todos) => {
todos = filterTodos(todos, getFilter());
setStore((s) => {
const todos = filterTodos(s, s.filter);
if (todos.every(({ done }) => done)) {
todos.forEach((todo) => {
todo.done = false;
Expand Down
12 changes: 12 additions & 0 deletions examples/TodoApp/store/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { setStore, useStore } from "..";
import { Filter } from "./types";

export const useFilter = () => {
return useStore((state) => state.filter);
};

export const setFilter = (filter: string) => {
setStore((s) => {
s.filter = filter as Filter;
});
};
5 changes: 5 additions & 0 deletions examples/TodoApp/store/filter/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const initFilter = "All";

export const filters = [initFilter, "Active", "Completed"] as const;

export type Filter = (typeof filters)[number];
19 changes: 19 additions & 0 deletions examples/TodoApp/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import create from "create-global-state";
import immer from "create-global-state/lib/immer";
import { Filter, initFilter } from "./filter/types";
import { initTodo } from "./todos/types";
import { compose } from "create-global-state/lib/utils";
import logger from "./logger";

export const initStore = {
todos: [initTodo],
filter: initFilter as Filter,
};

export type Store = typeof initStore;

const [useStore, baseSetStore] = create(initStore);

const setStore = compose(baseSetStore, immer, logger);

export { useStore, setStore };
15 changes: 15 additions & 0 deletions examples/TodoApp/store/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { producer } from "create-global-state";
import { SetStoreX } from "create-global-state/lib/utils";

// Create a custom middleware to log all the state changes.
export default function logger<S>(next: SetStoreX<S>): SetStoreX<S> {
return (ns, opts) => {
next((from) => {
const to = producer(ns)(from);

console.info("Change state", { from, to });

return to;
}, opts);
};
}
80 changes: 80 additions & 0 deletions examples/TodoApp/store/todos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { setStore, Store, useStore } from "..";
import { numbersEqual } from "./utils";
import { Filter } from "../filter/types";
import { useEqual } from "create-global-state/lib/utils";

// Get a todo by id.
export function useTodo(id: number) {
return useStore((s) => findTodo(s, id));
}

export function useTodoDone(id: number) {
return useStore((s) => findTodo(s, id).done);
}

// Get a new id list only when the ids of the todos change.
export function useTodoIds() {
return useStore(
useEqual((s) => {
return filterTodos(s, s.filter).map(({ id }) => id);
}, numbersEqual)
);
}

export function useZeroCount() {
return useStore((s) => {
return filterTodos(s, s.filter).map(({ id }) => id).length === 0;
});
}

// Get the count of the left todos.
export function useLeftCount() {
return useStore((s) => s.todos.filter(({ done }) => !done).length);
}

export function useZeroDone() {
return useStore((s) => !s.todos.some(({ done }) => done));
}

// Update the text of a todo by id.
export function updateTodo(id: number, text: string) {
setStore((s) => {
findTodo(s, id).text = text;
});
}

// 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;
}
});
}

// Delete a todo by id.
export function delTodo(id: number) {
setStore((s) => {
s.todos = s.todos.filter((todo) => todo.id !== id);
});
}

// Find a todo by id.
function findTodo(s: Store, id: number) {
return s.todos.find((todo) => todo.id === id)!;
}

// Filter the todos by the current status.
export function filterTodos(s: Store, filter: Filter) {
return s.todos.filter(({ done }) => {
switch (filter) {
case "All":
return true;
case "Active":
return !done;
case "Completed":
return done;
}
});
}
7 changes: 7 additions & 0 deletions examples/TodoApp/store/todos/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const initTodo = {
id: Date.now(),
text: "",
done: false,
};

export type Todos = (typeof initTodo)[];
File renamed without changes.
13 changes: 0 additions & 13 deletions examples/TodoApp/todos/filter.ts

This file was deleted.

Loading

0 comments on commit dd1d808

Please sign in to comment.