Skip to content

Commit

Permalink
Merge pull request #27 from szhsin/docs/useSelector
Browse files Browse the repository at this point in the history
Docs: useSelector
  • Loading branch information
szhsin authored May 9, 2023
2 parents faf0437 + 064deb0 commit c4fc0cc
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 46 deletions.
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Reactish-State

> Simple, decentralized state management for React.
> Simple, decentralized(atomic) state management for React.
## Install

Expand Down Expand Up @@ -94,9 +94,9 @@ The state management solutions in the React ecosystem have popularized two state

- **Centralized**: a single store that combines entire app states together and slices of the store are connected to React components through selectors. Examples: react-redux, Zustand.

- **Decentralized**: consisting of many small states which can build up state dependency trees using a bottom-up approach. React components only need to connect with the states that they use. Examples: Recoil, Jotai.
- **Decentralized**: consisting of many small(atomic) states which can build up state dependency trees using a bottom-up approach. React components only need to connect with the states that they use. Examples: Recoil, Jotai.

This library adopts the decentralized state model, offering a _Recoil-like_ API, but with a much simpler and smaller implementation(similar to Zustand), which makes it the one of the smallest state management solutions with gzipped bundle size less than 1KB.
This library adopts the decentralized state model, offering a _Recoil-like_ API, but with a much simpler and smaller implementation(similar to Zustand), which makes it the one of the smallest state management solutions with gzipped bundle size around 1KB.

| | State model | Bundle size |
| --- | --- | --- |
Expand Down Expand Up @@ -153,7 +153,7 @@ The difference might sound insignificant, but imaging every single state update
- Feature extensible with middleware or plugins
- States persistable to browser storage
- Support Redux dev tools via middleware
- Less than 1KB: simple and small
- [~1KB](https://bundlephobia.com/package/reactish-state): simple and small

# Recipes

Expand Down Expand Up @@ -303,6 +303,55 @@ const Example = () => {
};
```

## Selector that depends on props or local states

The `selector` function allows us to create reusable derived states outside React components. In contrast, component-scoped derived states which depend on props or local states can be created by the `useSelector` hook.

```jsx
import { state, useSelector } from "reactish-state";

const todosState = state([{ task: "Shop groceries", completed: false }]);

const FilteredTodoList = ({ filter = "ALL" }) => {
const filteredTodos = useSelector(
() => [
todosState,
(todos) => {
switch (filter) {
case "ALL":
return todos;
case "COMPLETED":
return todos.filter((todo) => todo.completed);
case "ACTIVE":
return todos.filter((todo) => !todo.completed);
}
}
],
[filter]
);
// Render filtered todos...
};
```

The second parameter of `useSelector` is a dependency array (similar to React's `useMemo` hook), in which you can specify what props or local states the selector depends on. In the above example, `FilteredTodoList` component will re-render only if the global `todosState` state or local `filter` prop have been updated.

### Linting the dependency array of useSelector

You can take advantage of the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package to lint the dependency array of `useSelector`. Add the following configuration into your ESLint config file:

```json
{
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useSelector"
}
]
}
}
```

## Still perfer Redux-like reducers?

```js
Expand Down
8 changes: 7 additions & 1 deletion examples/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ module.exports = {
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-non-null-assertion': 0
'@typescript-eslint/no-non-null-assertion': 0,
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: 'useSelector'
}
]
}
}
]
Expand Down
41 changes: 23 additions & 18 deletions examples/examples/todo/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { useSnapshot } from 'reactish-state';
import { todoListState, visibleTodoList } from './store';
import { useSelector } from 'reactish-state';
import { todoListState, getTodoListByFilter, VisibilityFilter } from './store';
import styles from './styles.module.css';

const TodoList = () => {
const todos = useSnapshot(visibleTodoList);
const { toggleItem, deleteItem } = todoListState.actions;
// Component-scoped derived states which depend on props or local states can be created by the `useSelector` hook
const TodoList = ({ filter = 'ALL' }: { filter?: VisibilityFilter }) => {
const todos = useSelector(
() => [todoListState, (todos) => getTodoListByFilter(todos, filter)],
[filter]
);

return (
<ul className={styles.todos}>
{todos.map(({ id, text, isCompleted }) => (
<li key={id} className={styles.todo}>
<label className={styles.todoLabel}>
<input type="checkbox" checked={isCompleted} onChange={() => toggleItem(id)} />
<span className={isCompleted ? styles.completed : ''}>{text}</span>
</label>
<button className={styles.delete} onClick={() => deleteItem(id)}>
Delete
</button>
</li>
))}
</ul>
<>
<hr />
<h2>{filter} todos</h2>
{todos.length ? (
<ul className={styles.todos}>
{todos.map(({ id, text, isCompleted }) => (
<li key={id} className={`${styles.todoItem} ${isCompleted && styles.completed}`}>
{text}
</li>
))}
</ul>
) : (
<p>There are no {filter} todos to display.</p>
)}
</>
);
};

Expand Down
26 changes: 26 additions & 0 deletions examples/examples/todo/VisibleTodoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useSnapshot } from 'reactish-state';
import { todoListState, visibleTodoList } from './store';
import styles from './styles.module.css';

const VisibleTodoList = () => {
const todos = useSnapshot(visibleTodoList);
const { toggleItem, deleteItem } = todoListState.actions;

return (
<ul className={styles.todos}>
{todos.map(({ id, text, isCompleted }) => (
<li key={id} className={styles.todo}>
<label className={styles.todoLabel}>
<input type="checkbox" checked={isCompleted} onChange={() => toggleItem(id)} />
<span className={isCompleted ? styles.completed : ''}>{text}</span>
</label>
<button className={styles.delete} onClick={() => deleteItem(id)}>
Delete
</button>
</li>
))}
</ul>
);
};

export { VisibleTodoList };
9 changes: 6 additions & 3 deletions examples/examples/todo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { useEffect } from 'react';
import { hydrateStore } from './store';
import { AddTodo } from './AddTodo';
import { TodoList } from './TodoList';
import { VisibleTodoList } from './VisibleTodoList';
import { Filters } from './Filters';
import { Stats } from './Stats';
import { TodoList } from './TodoList';
import styles from './styles.module.css';

export default function Todo() {
export default function App() {
useEffect(() => {
hydrateStore();
}, []);

return (
<div className={styles.app}>
<AddTodo />
<TodoList />
<VisibleTodoList />
<Filters />
<Stats />
<TodoList filter="ACTIVE" />
<TodoList filter="COMPLETED" />
</div>
);
}
37 changes: 21 additions & 16 deletions examples/examples/todo/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,21 @@ const todoListState = state(
type VisibilityFilter = 'ALL' | 'ACTIVE' | 'COMPLETED';
const visibilityFilterState = state('ALL' as VisibilityFilter, null, { key: 'filter' });

const getTodoListByFilter = (todoList: Todo[], filter: VisibilityFilter) => {
switch (filter) {
case 'ALL':
return todoList;
case 'COMPLETED':
return todoList.filter(({ isCompleted }) => isCompleted);
case 'ACTIVE':
return todoList.filter(({ isCompleted }) => !isCompleted);
}
};

const selector = createSelector({ plugin: devtoolsPlugin({ name: 'todoApp-selector' }) });
const visibleTodoList = selector(
todoListState,
visibilityFilterState,
(todoList, visibilityFilter) => {
switch (visibilityFilter) {
case 'ALL':
return todoList;
case 'COMPLETED':
return todoList.filter(({ isCompleted }) => isCompleted);
case 'ACTIVE':
return todoList.filter(({ isCompleted }) => !isCompleted);
}
},
{ key: 'visibleTodos' }
);
const visibleTodoList = selector(todoListState, visibilityFilterState, getTodoListByFilter, {
key: 'visibleTodos'
});

const statsSelector = selector(
todoListState,
Expand All @@ -83,4 +82,10 @@ const statsSelector = selector(

export type { VisibilityFilter };
export const { hydrate: hydrateStore } = persistMiddleware;
export { todoListState, visibilityFilterState, visibleTodoList, statsSelector };
export {
todoListState,
visibilityFilterState,
visibleTodoList,
statsSelector,
getTodoListByFilter
};
5 changes: 5 additions & 0 deletions examples/examples/todo/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
flex-grow: 1;
}

.todoItem {
margin: 0.5rem 0.25rem;
list-style-position: inside;
}

.completed {
text-decoration: line-through;
}
Expand Down
2 changes: 1 addition & 1 deletion examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reactish-state",
"version": "0.11.0-alpha.0",
"version": "0.11.0",
"description": "Simple, decentralized state management for React.",
"author": "Zheng Song",
"license": "MIT",
Expand Down

0 comments on commit c4fc0cc

Please sign in to comment.