Add new post

This commit is contained in:
DefectingCat
2022-10-14 14:16:59 +08:00
parent 891905529a
commit 024836d5d4
4 changed files with 369 additions and 4 deletions

View File

@ -5,6 +5,12 @@ import {
resetStyles,
} from 'data/sandpack/how-to-load-a-background-with-threejs';
import { Button, Input } from './sandpack';
import {
miniRedux,
multi,
MultiStore,
Reducer,
} from './sandpack/build-own-store-with-usesyncexternalstore';
import {
genericApp,
genericChild,
@ -46,6 +52,12 @@ const data = {
useSyncExternalStore,
useInsertionEffect,
},
'build-own-store-with-usesyncexternalstore': {
multi,
miniRedux,
MultiStore,
Reducer,
},
},
};

View File

@ -142,6 +142,22 @@ const Couter = () => {
};
```
<RUASandpack
template="react-ts"
files={{
'/Input.tsx': {
code: sandpack.common.Input,
hidden: true,
},
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/store.ts': sandpack['react18-new-hooks'].store,
'/App.tsx': sandpack['react18-new-hooks'].useSyncExternalStore,
}}
/>
## 多个 Store
上面的实现是直接针对单一的 store 来实现的,直接将 state 和其方法封装在一个对象中。日常的项目中通常会根据功能来创建多个全局状态,避免混乱。
@ -211,7 +227,7 @@ export const useTodoStore = (): [Todo, SetState<Todo>] => [
];
```
这里的 `useTodoStore` 将其显式的注解了其返回值,因为返回的是和 `useState` 类似的元组,而默认 TypeScript 推断的类型比较宽松,会推断为 `(*Todo* | *SetState*<*Todo*>)[]` 的数组。
这里的 `useTodoStore` 将其显式的注解了其返回值,因为返回的是和 `useState` 类似的元组,而默认 TypeScript 推断的类型比较宽松,会推断为 `(Todo | SetState<Todo>)[]` 的数组。
由于封装了新的 hook在组件中的使用也就更方便了。在需要不同 store 的组件中直接使用不同的 hook 就能访问到对应的全局状态了。
@ -226,6 +242,23 @@ const Count = () => {
};
```
<RUASandpack
template="react-ts"
files={{
'/Input.tsx': {
code: sandpack.common.Input,
hidden: true,
},
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/multi.ts': sandpack['build-own-store-with-usesyncexternalstore'].multi,
'/App.tsx':
sandpack['build-own-store-with-usesyncexternalstore'].MultiStore,
}}
/>
## Mini Redux
在模仿 Reudx 之前,应该再熟悉一下 Redux 的工作流程。在 Redux 中,和我们上述的状态一样,状态都是普通对象,且是不可变的数据(只读)。此外,我们的 reducer 也应该保持是纯函数。
@ -340,6 +373,23 @@ export const useTodoStore = (): [Todo, RUADispatch<TodoAction>] => [
];
```
<RUASandpack
template="react-ts"
files={{
'/Input.tsx': {
code: sandpack.common.Input,
hidden: true,
},
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/store.ts':
sandpack['build-own-store-with-usesyncexternalstore'].miniRedux,
'/App.tsx': sandpack['build-own-store-with-usesyncexternalstore'].Reducer,
}}
/>
只是在 Redux 的三个原则中:
- Single source of truth

View File

@ -0,0 +1,302 @@
export const multi = `import { useSyncExternalStore } from 'react';
export type SetState<S> = (stateOrFn: S | ((state: S) => S)) => void;
export type GetSnapshot<S> = () => S;
export type Subscribe = (listener: () => void) => () => void;
export type CreateStore = <T extends Record<string, unknown> | unknown[]>(
initialState: T
) => {
getSnapshot: GetSnapshot<T>;
setState: SetState<T>;
subscribe: Subscribe;
};
export const createStore: CreateStore = <
T extends Record<string, unknown> | unknown[]
>(
initialState: T
) => {
let state = initialState;
const listeners = new Set<() => void>();
const getSnapshot = () => state;
const setState: SetState<T> = (stateOrFn) => {
state = typeof stateOrFn === 'function' ? stateOrFn(state) : stateOrFn;
listeners.forEach((listener) => listener());
};
const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
return {
getSnapshot,
setState,
subscribe,
};
};
export type Todo = {
id: number;
content: string;
}[];
const initialTodo: Todo = [
{ id: 0, content: 'React' },
{ id: 1, content: 'Vue' },
];
const todoStore = createStore(initialTodo);
export const useTodoStore = (): [Todo, SetState<Todo>] => [
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
todoStore.setState,
];
type Count = {
count: number;
info: string;
};
const initialCount: Count = {
count: 0,
info: 'Hello',
};
const countStore = createStore(initialCount);
export const useCountStore = (): [Count, SetState<Count>] => [
useSyncExternalStore(countStore.subscribe, countStore.getSnapshot),
countStore.setState,
];`;
export const miniRedux = `import { useSyncExternalStore } from 'react';
export type RUAState = Record<string, unknown> | unknown[];
export type RUAAction<P = unknown, T extends string = string> = {
payload: P;
type: T;
};
export type RUAReducer<S extends RUAState, A extends RUAAction> = (
state: S,
action: A
) => S;
export type RUADispatch<A extends RUAAction> = (action: A) => void;
export type GetSnapshot<S> = () => S;
export type Subscribe = (listener: () => void) => () => void;
export const createStore = <S extends RUAState, A extends RUAAction>(
reducer: RUAReducer<S, A>,
initialState: S
) => {
let state = initialState;
const listeners = new Set<() => void>();
const getSnapshot = () => state;
const dispatch: RUADispatch<A> = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
return {
subscribe,
getSnapshot,
dispatch,
};
};
export type Todo = {
id: number;
content: string;
}[];
const initialTodo: Todo = [
{ id: 0, content: 'React' },
{ id: 1, content: 'Vue' },
];
export type TodoAction = RUAAction<number | string, 'add' | 'delete'>;
const reducer: RUAReducer<Todo, TodoAction> = (state, action) => {
switch (action.type) {
case 'add': {
if (action.payload == null) throw new Error('Add todo without payload!');
return [
...state,
{
id: state[state.length - 1].id + 1,
content: action.payload.toString(),
},
];
}
case 'delete': {
if (action.payload == null)
throw new Error('Delete todo without payload!');
return state.filter((todo) => todo.id !== action.payload);
}
default:
throw new Error('Dispatch a reducer without action!');
}
};
const todoStore = createStore(reducer, initialTodo);
export const useTodoStore = (): [Todo, RUADispatch<TodoAction>] => [
useSyncExternalStore(todoStore.subscribe, todoStore.getSnapshot),
todoStore.dispatch,
];`;
export const MultiStore = `import Button from './Button';
import Input from './Input';
import { useState } from 'react';
import { useCountStore, useTodoStore } from './multi';
const Todo = () => {
const [todos, setTodo] = useTodoStore();
const [value, setValue] = useState('');
const handleAdd = () => {
if (!value) return;
setTodo((d) => [...d, { id: d[d.length - 1].id + 1, content: value }]);
setValue('');
};
return (
<>
<div>
<ul>
{todos.map((todo) => (
<div key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
}}
className="flex items-center mb-2"
>
<li className="mr-2">{todo.content}</li>
<Button
onClick={() =>
setTodo((d) => d.filter((item) => item.id !== todo.id))
}
>
Delete
</Button>
</div>
))}
</ul>
<div>
<Input
type="text"
className="mr-1"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Button onClick={handleAdd}>Add</Button>
</div>
</div>
</>
);
};
const Count = () => {
const [{ count, info }, setState] = useCountStore();
return (
<>
<div>
<div>
Count: <span>{count}</span>
</div>
<div>
Info: <span>{info}</span>
</div>
<div>
<Button
onClick={() => setState((d) => ({ ...d, count: d.count + 1 }))}
>
Add
</Button>
</div>
<div>
<Input
type="text"
onChange={(e) => setState((d) => ({ ...d, info: e.target.value }))}
value={info}
/>
</div>
</div>
</>
);
};
const MultiStore = () => {
return (
<>
<div className="p-4">
<Todo />
<hr className="my-4" />
<Count />
</div>
</>
);
};
export default MultiStore;`;
export const Reducer = `import Button from './Button';
import Input from './Input';
import { useState } from 'react';
import { useTodoStore } from './store.ts';
const Reducer = () => {
const [todos, dispatch] = useTodoStore();
const [value, setValue] = useState('');
const handleAdd = () => {
if (!value) return;
dispatch({
type: 'add',
payload: value,
});
setValue('');
};
return (
<>
<div className="p-4">
<div>
<ul>
{todos.map((todo) => (
<div key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
}}
className="flex items-center mb-2"
>
<li className="mr-2">{todo.content}</li>
<Button
onClick={() => dispatch({ type: 'delete', payload: todo.id })}
>
Delete
</Button>
</div>
))}
</ul>
<div>
<Input
type="text"
className="mr-1"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Button onClick={handleAdd}>Add</Button>
</div>
</div>
</div>
</>
);
};
export default Reducer;`;

View File

@ -113,7 +113,7 @@ export const store = `export type State = {
export type Store = {
state: State;
setState: (
stateOrFn: Partial<State> | ((state: State) => Partial<State>)
stateOrFn: State | ((state: State) => State)
) => void;
subscribe: (listener: () => void) => () => void;
listeners: Set<() => void>;
@ -170,7 +170,7 @@ const Couter = () => {
<div>
<Button
onClick={() => store.setState((d) => ({ count: d.count + 1 }))}
onClick={() => store.setState((d) => ({ ...d, count: d.count + 1 }))}
>
Add
</Button>
@ -199,7 +199,8 @@ const Infor = () => {
<div>
<Input
type="text"
onChange={(e) => store.setState({ info: e.target.value })}
onChange={(e) => store.setState((d) => ({ ...d, info: e.target.value }))}
value={info}
/>
</div>
</div>