mirror of
https://github.com/DefectingCat/DefectingCat.github.io
synced 2025-07-16 09:11:38 +00:00
Add new post
This commit is contained in:
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
302
data/sandpack/build-own-store-with-usesyncexternalstore.ts
Normal file
302
data/sandpack/build-own-store-with-usesyncexternalstore.ts
Normal 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;`;
|
@ -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>
|
||||
|
Reference in New Issue
Block a user