Skip to content

Commit 34c8b02

Browse files
committed
feat: basic func
1 parent 8f8df90 commit 34c8b02

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

docs/examples/simple.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,46 @@
1-
export default () => 'Hello World';
1+
import { createContext, useContextSelector } from '@rc-component/context';
2+
import React from 'react';
3+
4+
const CountContext = createContext<{
5+
cnt1: number;
6+
cnt2: number;
7+
}>();
8+
9+
const useRenderTimes = () => {
10+
const renderRef = React.useRef(0);
11+
renderRef.current += 1;
12+
13+
return renderRef.current;
14+
};
15+
16+
const MyConsumer = React.memo(({ name }: { name: any }) => {
17+
const value = useContextSelector(CountContext, name);
18+
const renderTimes = useRenderTimes();
19+
20+
return (
21+
<div>
22+
<label>{JSON.stringify(name)}:</label>
23+
{value} ({renderTimes} times)
24+
</div>
25+
);
26+
});
27+
28+
export default () => {
29+
const [cnt1, setCnt1] = React.useState(0);
30+
const [cnt2, setCnt2] = React.useState(0);
31+
const renderTimes = useRenderTimes();
32+
33+
return (
34+
<CountContext.Provider value={{ cnt1, cnt2 }}>
35+
<button type="button" onClick={() => setCnt1(v => v + 1)}>
36+
cnt 1: {cnt1}
37+
</button>
38+
<button type="button" onClick={() => setCnt2(v => v + 1)}>
39+
cnt 2: {cnt2}
40+
</button>
41+
{renderTimes} times
42+
<MyConsumer name="cnt1" />
43+
<MyConsumer name="cnt2" />
44+
</CountContext.Provider>
45+
);
46+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
"dependencies": {
3838
"@babel/runtime": "^7.10.1",
39-
"rc-util": "^5.26.0"
39+
"rc-util": "^5.27.0"
4040
},
4141
"devDependencies": {
4242
"@rc-component/father-plugin": "^1.0.0",

src/context.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import useEvent from 'rc-util/lib/hooks/useEvent';
2+
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
3+
import isEqual from 'rc-util/lib/isEqual';
4+
import * as React from 'react';
5+
import { unstable_batchedUpdates } from 'react-dom';
6+
7+
export type Selector<ContextProps, SelectorValue = ContextProps> = (
8+
value: ContextProps,
9+
) => SelectorValue;
10+
11+
export type Trigger<ContextProps> = (value: ContextProps) => void;
12+
13+
export type Listeners<ContextProps> = Set<Trigger<ContextProps>>;
14+
15+
export interface Context<ContextProps> {
16+
getValue: () => ContextProps;
17+
listeners: Listeners<ContextProps>;
18+
}
19+
20+
export interface ContextSelectorProviderProps<T> {
21+
value: T;
22+
children?: React.ReactNode;
23+
}
24+
25+
export interface SelectorContext<ContextProps> {
26+
Context: React.Context<Context<ContextProps>>;
27+
Provider: React.ComponentType<ContextSelectorProviderProps<ContextProps>>;
28+
}
29+
30+
export function createContext<ContextProps>(
31+
defaultContext?: ContextProps,
32+
): SelectorContext<ContextProps> {
33+
const Context = React.createContext<Context<ContextProps>>(defaultContext as any);
34+
35+
const Provider = ({ value, children }: ContextSelectorProviderProps<ContextProps>) => {
36+
const valueRef = React.useRef(value);
37+
valueRef.current = value;
38+
39+
const [context] = React.useState<Context<ContextProps>>(() => ({
40+
getValue: () => valueRef.current,
41+
listeners: new Set(),
42+
}));
43+
44+
useLayoutEffect(() => {
45+
unstable_batchedUpdates(() => {
46+
context.listeners.forEach(listener => {
47+
listener(value);
48+
});
49+
});
50+
}, [value]);
51+
52+
return <Context.Provider value={context}>{children}</Context.Provider>;
53+
};
54+
55+
return { Context, Provider };
56+
}
57+
58+
/** e.g. useSelect(userContext, user => user.name) => user.name */
59+
export function useContextSelector<ContextProps, SelectorValue>(
60+
holder: SelectorContext<ContextProps>,
61+
selector: Selector<ContextProps, SelectorValue>,
62+
): SelectorValue;
63+
64+
/** e.g. useSelect(userContext, ['name', 'age']) => user { name, age } */
65+
export function useContextSelector<ContextProps, SelectorValue extends Partial<ContextProps>>(
66+
holder: SelectorContext<ContextProps>,
67+
selector: (keyof ContextProps)[],
68+
): SelectorValue;
69+
70+
/** e.g. useSelect(userContext, 'name') => user.name */
71+
export function useContextSelector<ContextProps, PropName extends keyof ContextProps>(
72+
holder: SelectorContext<ContextProps>,
73+
selector: PropName,
74+
): ContextProps[PropName];
75+
76+
export function useContextSelector<ContextProps, SelectorValue>(
77+
holder: SelectorContext<ContextProps>,
78+
selector: Selector<ContextProps, any> | (keyof ContextProps)[] | keyof ContextProps,
79+
) {
80+
const eventSelector = useEvent<Selector<ContextProps, SelectorValue>>(
81+
typeof selector === 'function'
82+
? selector
83+
: ctx => {
84+
if (!Array.isArray(selector)) {
85+
return ctx[selector];
86+
}
87+
88+
const obj = {} as SelectorValue;
89+
selector.forEach(key => {
90+
(obj as any)[key] = ctx[key];
91+
});
92+
return obj;
93+
},
94+
);
95+
const context = React.useContext(holder?.Context);
96+
const { listeners, getValue } = context || {};
97+
98+
const valueRef = React.useRef<SelectorValue>();
99+
valueRef.current = eventSelector(context ? getValue() : null);
100+
const [, forceUpdate] = React.useState({});
101+
102+
useLayoutEffect(() => {
103+
if (!context) {
104+
return;
105+
}
106+
107+
function trigger(nextValue: ContextProps) {
108+
const nextSelectorValue = eventSelector(nextValue);
109+
if (!isEqual(valueRef.current, nextSelectorValue, true)) {
110+
forceUpdate({});
111+
}
112+
}
113+
114+
listeners.add(trigger);
115+
116+
return () => {
117+
listeners.delete(trigger);
118+
};
119+
}, [context]);
120+
121+
return valueRef.current;
122+
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { SelectorContext } from './context';
2+
import { createContext, useContextSelector } from './context';
3+
4+
export { createContext, useContextSelector };
5+
export type { SelectorContext };

0 commit comments

Comments
 (0)