Skip to content

Commit

Permalink
feat: useSelector([])
Browse files Browse the repository at this point in the history
  • Loading branch information
acrazing committed Oct 23, 2024
1 parent 5ca8c57 commit 25feea0
Show file tree
Hide file tree
Showing 22 changed files with 119 additions and 133 deletions.
1 change: 1 addition & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarn tsx ./scripts/check_apps.ts
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const countBox = numberBox('count');

function Count() {
const dispatch = useDispatch();
const [count] = useSelector(countBox);
const count = useSelector(countBox);

return (
<div>
Expand Down
4 changes: 2 additions & 2 deletions examples/Counter/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

import { numberBox } from 'amos';
import { useDispatch, useSelector } from 'amos/react';
import React, { memo } from 'react';
import { memo } from 'react';

const countBox = numberBox('count', 0);

export const App = memo(() => {
const dispatch = useDispatch();
const [count] = useSelector(countBox);
const count = useSelector(countBox);
return (
<div>
<span>Count: {count}&nbsp;</span>
Expand Down
6 changes: 3 additions & 3 deletions examples/Counter/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createStore } from 'amos-core';
import { Provider } from 'amos-react';
import React, { StrictMode } from 'react';
import { createStore } from 'amos';
import { Provider } from 'amos/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoMVC/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*/

import { useSelector } from 'amos/react';
import React, { memo } from 'react';
import { memo } from 'react';
import { Filter } from './components/Filter';
import { Header } from './components/Header';
import { SignIn } from './components/SignIn';
import { TodoList } from './components/TodoList';
import { currentUserIdBox } from './store/user.boxes';

export const App = memo(() => {
const [userId] = useSelector(currentUserIdBox);
const userId = useSelector(currentUserIdBox);
if (!userId) {
return <SignIn />;
}
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoMVC/src/components/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
*/

import { useDispatch, useSelector } from 'amos/react';
import React, { memo, useState } from 'react';
import { memo, useState } from 'react';
import { addTodo } from '../store/todo.actions';
import { TodoStatusFilter, todoStatusFilterBox } from '../store/todo.boxes';

export const Filter = memo(() => {
const dispatch = useDispatch();
const [status] = useSelector(todoStatusFilterBox);
const status = useSelector(todoStatusFilterBox);
const [input, setInput] = useState('');
return (
<div>
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoMVC/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
*/

import { useDispatch, useSelector } from 'amos/react';
import React, { memo } from 'react';
import { memo } from 'react';
import { signOut } from '../store/user.actions';
import { selectCurrentUser } from '../store/user.selectors';

export const Header = memo(() => {
const dispatch = useDispatch();
const [user] = useSelector(selectCurrentUser());
const user = useSelector(selectCurrentUser());
const handleSignOut = () => {
const keepData = confirm('You will be signed out, do you want to keep your data?');
dispatch(signOut(keepData));
Expand Down
2 changes: 1 addition & 1 deletion examples/TodoMVC/src/components/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { useDispatch } from 'amos/react';
import React, { memo, useState } from 'react';
import { memo, useState } from 'react';
import { signIn } from '../store/user.actions';

export const SignIn = memo(() => {
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoMVC/src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
*/

import { useDispatch, useSelector } from 'amos/react';
import React, { memo } from 'react';
import { memo } from 'react';
import { completeTodo, deleteTodo } from '../store/todo.actions';
import { todoMapBox } from '../store/todo.boxes';

export const TodoItem = memo(({ id }: { id: number }) => {
const dispatch = useDispatch();
const [todo] = useSelector(todoMapBox.getItem(id));
const todo = useSelector(todoMapBox.getItem(id));
return (
<div>
<span>{todo.title}</span>
Expand Down
2 changes: 1 addition & 1 deletion examples/TodoMVC/src/components/TodoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { useQuery } from 'amos/react';
import React, { memo } from 'react';
import { memo } from 'react';
import { getTodoList } from '../store/todo.actions';
import { TodoItem } from './TodoItem';

Expand Down
5 changes: 2 additions & 3 deletions examples/TodoMVC/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createStore } from 'amos-core';
import { IDBStorage, withPersist } from 'amos-persist';
import { createStore, IDBStorage, withPersist } from 'amos';
import { Provider } from 'amos/react';
import React, { StrictMode } from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
Expand Down
4 changes: 1 addition & 3 deletions examples/TodoMVC/src/store/todo.boxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
* @author junbao <[email protected]>
*/

import { listMapBox, recordMapBox } from 'amos-boxes';
import { box } from 'amos-core';
import { Record } from 'amos-shapes';
import { box, listMapBox, Record, recordMapBox } from 'amos';
import { signOutSignal } from './user.boxes';

export function hashCode(s: string) {
Expand Down
6 changes: 3 additions & 3 deletions packages/amos-core/src/enhancers/withCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Entry, isAmosObject, override } from 'amos-utils';
import { StoreEnhancer } from '../store';
import { resolveCacheKey } from '../utils';

export type SelectValueEntry<R = any> = Entry<Selectable<R>, R>;
export type SelectEntry<R = any> = Entry<Selectable<R>, R>;

export function isSelectValueEqual<R>(s: Selectable<R>, a: R, b: R) {
if (!isAmosObject<Selector>(s, 'selector') || s.cache) {
Expand All @@ -20,8 +20,8 @@ export function isSelectValueEqual<R>(s: Selectable<R>, a: R, b: R) {
export const withCache: () => StoreEnhancer = () => {
return (next) => (options) => {
const store = next(options);
const cacheMap = new Map<string, readonly [value: any, deps: readonly SelectValueEntry[]]>();
const stack: Array<SelectValueEntry[] | null> = [];
const cacheMap = new Map<string, readonly [value: any, deps: readonly SelectEntry[]]>();
const stack: Array<SelectEntry[] | null> = [];
const peak = () => stack[stack.length - 1];
override(store, 'select', (select) => {
return (s: any) => {
Expand Down
40 changes: 14 additions & 26 deletions packages/amos-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,14 @@ export interface DispatchableRecord<R> {

export type Dispatchable<R = any> = DispatchableRecord<R>[keyof DispatchableRecord<R>];

export type MapDispatchable<Rs extends readonly Dispatchable[]> = {
[P in keyof Rs]: Rs[P] extends Dispatchable<infer R> ? R : never;
};

export interface Dispatch {
<R>(task: Dispatchable<R>): R;
<R1>(tasks: readonly [Dispatchable<R1>]): [R1];
<R1, R2>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>]): [R1, R2];
<R1, R2, R3>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>]): [R1, R2, R3];
<R1, R2, R3, R4>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>]): [R1, R2, R3, R4];
<R1, R2, R3, R4, R5>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>, Dispatchable<R5>]): [R1, R2, R3, R4, R5];
<R1, R2, R3, R4, R5, R6>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>, Dispatchable<R5>, Dispatchable<R6>]): [R1, R2, R3, R4, R5, R6];
<R1, R2, R3, R4, R5, R6, R7>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>, Dispatchable<R5>, Dispatchable<R6>, Dispatchable<R7>]): [R1, R2, R3, R4, R5, R6, R7];
<R1, R2, R3, R4, R5, R6, R7, R8>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>, Dispatchable<R5>, Dispatchable<R6>, Dispatchable<R7>, Dispatchable<R8>]): [R1, R2, R3, R4, R5, R6, R7, R8];
<R1, R2, R3, R4, R5, R6, R7, R8, R9>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>, Dispatchable<R5>, Dispatchable<R6>, Dispatchable<R7>, Dispatchable<R8>, Dispatchable<R9>]): [R1, R2, R3, R4, R5, R6, R7, R8, R9];
<R>(tasks: readonly Dispatchable<R>[]): R[];
<R>(dispatchable: Dispatchable<R>): R;
<Rs extends readonly Dispatchable[]>(dispatchables: Rs): MapDispatchable<Rs>;
<R>(dispatchables: readonly Dispatchable<R>[]): R[];
}

export interface SelectableRecord<R> {
Expand All @@ -38,23 +34,15 @@ export interface SelectableRecord<R> {

export type Selectable<R = any> = SelectableRecord<R>[keyof SelectableRecord<R>];

export interface Select {
<R>(selector: Selectable<R>): R;
<R1>(selector: readonly [Selectable<R1>]): [R1];
<R1, R2>(selector: readonly [Selectable<R1>, Selectable<R2>]): [R1, R2];
<R1, R2, R3>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>]): [R1, R2, R3];
<R1, R2, R3, R4>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>]): [R1, R2, R3, R4];
<R1, R2, R3, R4, R5>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>, Selectable<R5>]): [R1, R2, R3, R4, R5];
<R1, R2, R3, R4, R5, R6>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>, Selectable<R5>, Selectable<R6>]): [R1, R2, R3, R4, R5, R6];
<R1, R2, R3, R4, R5, R6, R7>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>, Selectable<R5>, Selectable<R6>, Selectable<R7>]): [R1, R2, R3, R4, R5, R6, R7];
<R1, R2, R3, R4, R5, R6, R7, R8>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>, Selectable<R5>, Selectable<R6>, Selectable<R7>, Selectable<R8>]): [R1, R2, R3, R4, R5, R6, R7, R8];
<R1, R2, R3, R4, R5, R6, R7, R8, R9>(selector: readonly [Selectable<R1>, Selectable<R2>, Selectable<R3>, Selectable<R4>, Selectable<R5>, Selectable<R6>, Selectable<R7>, Selectable<R8>, Selectable<R9>]): [R1, R2, R3, R4, R5, R6, R7, R8, R9];
<R>(selector: readonly Selectable<R>[]): R[];
}

export type MapSelector<Rs extends readonly Selectable[]> = {
export type MapSelectable<Rs extends readonly Selectable[]> = {
[P in keyof Rs]: Rs[P] extends Selectable<infer R> ? R : never;
};

export interface Select {
<R>(selectable: Selectable<R>): R;
<Rs extends readonly Selectable[]>(selectables: Rs): MapSelectable<Rs>;
<R>(selectables: readonly Selectable<R>[]): R[];
}

export type CacheKey = ValueOrReadonlyArray<ID>;
export type CacheOptions<A extends any[]> = ValueOrReadonlyArray<Selectable<string> | Selectable<number>> | SelectorFactory<A, CacheKey> | Compute<A, CacheKey>;
4 changes: 2 additions & 2 deletions packages/amos-persist/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* @author junbao <[email protected]>
*/

import { Action, action, Box, Dispatch, MapSelector, Select } from 'amos-core';
import { Action, action, Box, Dispatch, MapSelectable, Select } from 'amos-core';
import { NotImplemented } from 'amos-utils';

export interface LoadBoxes {
<A extends Box[]>(...boxes: A): Action<A, Promise<MapSelector<A>>>;
<A extends Box[]>(...boxes: A): Action<A, Promise<MapSelectable<A>>>;
}

export const loadBoxes: LoadBoxes = action(
Expand Down
1 change: 0 additions & 1 deletion packages/amos-react/src/context.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { render } from '@testing-library/react';
import { createStore, Store } from 'amos-core';
import React from 'react';
import { Consumer, Provider } from './context';

describe('provider', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/amos-react/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Dispatch, Store } from 'amos';
import { must } from 'amos-utils';
import React, { createContext, ReactNode, useContext } from 'react';
import { createContext, ReactNode, useContext } from 'react';

export const Context = createContext<Store | null>(null);

Expand Down
10 changes: 5 additions & 5 deletions packages/amos-react/src/useSelector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { createStore, MapSelector, Select, Selectable, selector, Snapshot, Store } from 'amos-core';
import { createStore, MapSelectable, Select, Selectable, selector, Snapshot, Store } from 'amos-core';
import {
addTwiceAsync,
countBox,
Expand All @@ -13,7 +13,7 @@ import {
selectDoubleCount,
selectMultipleCount,
} from 'amos-testing';
import React, { FC } from 'react';
import { FC } from 'react';
import { Provider, useDispatch, useStore } from './context';
import { useSelector } from './useSelector';
import fn = jest.fn;
Expand All @@ -35,9 +35,9 @@ function renderUseSelector<P, Rs extends readonly Selectable[]>(
fn: (props: P) => Rs,
preloaded?: Snapshot,
initialProps?: P,
): RenderHookResult<P, MapSelector<Rs>> & Store {
): RenderHookResult<P, MapSelectable<Rs>> & Store {
const store = createStore({ preloadedState: preloaded });
const hook = renderHook((props: P) => useSelector(...fn(props)), {
const hook = renderHook((props: P) => useSelector(fn(props)), {
wrapper: (props: any) => <Provider store={store}>{props.children}</Provider>,
initialProps,
});
Expand All @@ -47,7 +47,7 @@ function renderUseSelector<P, Rs extends readonly Selectable[]>(
describe('useSelector', () => {
it('should works fine with WebStorm', () => {
renderHook(() => {
const [] = useSelector(countBox);
useSelector(countBox);
});
});
it('should select state', () => {
Expand Down
77 changes: 15 additions & 62 deletions packages/amos-react/src/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@ import {
Box,
isAmosObject,
isSelectValueEqual,
MapSelector,
Select,
Selectable,
Selector,
SelectValueEntry,
SelectEntry,
} from 'amos';
import { useCallback, useDebugValue, useLayoutEffect, useReducer, useRef } from 'react';
import { useStore } from './context';

export function useSelect(): Select {
function useSelect(): Select {
const [, update] = useReducer((s) => s + 1, 0);
const store = useStore();
const deps = useRef<SelectValueEntry[]>([]);
const deps = useRef<SelectEntry[]>([]);
const rendering = useRef(true);
rendering.current = true;
deps.current = [];
Expand Down Expand Up @@ -73,67 +72,21 @@ export function useSelect(): Select {
);
}

/**
* Get the {@link import('amos-core').Store.select} and any call to the
* returned selector in the render will be recorded and the component
* will re-render when the used selector's value changed.
*
* You can use the returned select function anywhere, including the
* conditional or loop blocks, and also in callbacks. Only the selector
* called in render function will trigger re-render.
*
* @example
* const select = useSelector();
* const foo = select(selectFoo());
* if (something) {
* select(bar())
* }
* return (
* <div onClick={() => alert(select(baz())}>
* {keys.map((key) => <div>{select(selectItem(key)).title</div>}
* </div>
* )
* ```
*/
export function useSelector(): Select;
/**
* Get the selected states according to the selectors, and rerender the
* component when the selected states updated.
*
* A selector is a selectable thing, it could be one of this:
*
* 1. A `Box` instance
* 2. A `Selector` which is created by `SelectorFactory`
*
* If the selector is a `Selector`, the selected state is its return value,
* otherwise, when the selector is a `Box`, the selected state is the state
* of the `Box`.
*
* `useSelector` accepts multiple selectors, and returns an array of the
* selected states of the selectors.
*
* @example
* ```typescript
* const [
* count, // 1
* tripleCount, // 2
* ] = useSelector(
* countBox, // A Box
* selectMultipleCount(3), // A Selector
* );
* ```
*/
export function useSelector<Rs extends Selectable[]>(...selectors: Rs): MapSelector<Rs>;
export function useSelector(...selectors: Selectable[]): any {
export interface UseSelector extends Select {
(): Select;
}

export const useSelector: UseSelector = (selectors?: Selectable | readonly Selectable[]): any => {
const select = useSelect();
const size = useRef(selectors.length);
if (size.current !== selectors.length) {
const size = Array.isArray(selectors) ? selectors.length : selectors === void 0 ? -2 : -1;
const sizeRef = useRef(size);
if (sizeRef.current !== size) {
throw new Error(
`selector size should be immutable, previous is ${size.current}, current is ${selectors.length}`,
`selector size should be immutable, previous is ${sizeRef.current}, current is ${size}`,
);
}
if (selectors.length === 0) {
if (!selectors) {
return select;
}
return select(selectors);
}
return select(selectors as any);
};
Loading

0 comments on commit 25feea0

Please sign in to comment.