-
Notifications
You must be signed in to change notification settings - Fork 13
/
LayoutGroup.tsx
115 lines (101 loc) · 3.05 KB
/
LayoutGroup.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import React, {
createContext,
useCallback,
useContext,
useLayoutEffect,
useRef,
} from "react";
import type { DependencyList, EffectCallback } from "react";
import { useForceUpdate } from "../hooks/useForceUpdate.js";
/**
* Like `useLayoutEffect`, but all effect executions are run
* *after* the `DeferredLayoutEffectsProvider` layout effects
* phase.
*
* This hook allows child components to enqueue layout effects
* that won't be safe to run until after a parent component's
* layout effects have run.
*
*/
const useLayoutGroupEffectsRegistry = () => {
const createQueue = useRef(new Set<() => void>()).current;
const destroyQueue = useRef(new Set<() => void>()).current;
const forceUpdate = useForceUpdate();
const isUpdatePending = useRef(true);
const ensureFlush = useCallback(() => {
if (!isUpdatePending.current) {
forceUpdate();
isUpdatePending.current = true;
}
}, [forceUpdate]);
const register = useCallback(
(effect: EffectCallback) => {
let destroy: ReturnType<EffectCallback>;
const create = () => {
destroy = effect();
};
createQueue.add(create);
ensureFlush();
return () => {
createQueue.delete(create);
if (destroy) {
destroyQueue.add(destroy);
ensureFlush();
}
};
},
[createQueue, destroyQueue, ensureFlush]
);
useLayoutEffect(() => {
isUpdatePending.current = false;
createQueue.forEach((create) => create());
createQueue.clear();
return () => {
destroyQueue.forEach((destroy) => destroy());
destroyQueue.clear();
};
});
return register;
};
type Destructor = () => void;
type Register = (e: EffectCallback) => Destructor;
const LayoutGroupContext = createContext<Register>(null as unknown as Register);
interface DeferredLayoutEffectsContextProviderProps {
children: React.ReactNode;
}
/**
* Provides a deferral point for deferred layout effects.
* All effects registered with `useDeferredLayoutEffect`
* by children of this provider will execute *after* all
* effects registered by `useLayoutEffect` by children of
* this provider.
*/
export function LayoutGroup({
children,
}: DeferredLayoutEffectsContextProviderProps) {
const register = useLayoutGroupEffectsRegistry();
return (
<LayoutGroupContext.Provider value={register}>
{children}
</LayoutGroupContext.Provider>
);
}
/**
* Like `useLayoutEffect`, but all effect executions are run
* *after* the `DeferredLayoutEffectsProvider` layout effects
* phase.
*
* This hook allows child components to enqueue layout effects
* that won't be safe to run until after a parent component's
* layout effects have run.
*/
export function useLayoutGroupEffect(
effect: EffectCallback,
deps?: DependencyList
) {
const register = useContext(LayoutGroupContext);
// The rule for hooks wants to statically verify the deps,
// but the dependencies are up to the caller, not this implementation.
// eslint-disable-next-line react-hooks/exhaustive-deps
useLayoutEffect(() => register(effect), deps);
}