Skip to content

Commit f4b8d01

Browse files
authored
fix: should not race with useLayoutEffect (#123)
* fix: should not race with useLayoutEffect * chore: optimize * chore: add test case * chore: rename
1 parent 797d3c1 commit f4b8d01

File tree

4 files changed

+92
-38
lines changed

4 files changed

+92
-38
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// import canUseDom from 'rc-util/lib/Dom/canUseDom';
2+
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
3+
import type { EffectCallback } from 'react';
4+
import * as React from 'react';
5+
6+
// We need fully clone React function here
7+
// to avoid webpack warning React 17 do not export `useId`
8+
const fullClone = {
9+
...React,
10+
};
11+
const { useInsertionEffect } = fullClone;
12+
13+
type UseCompatibleInsertionEffect = (
14+
renderEffect: EffectCallback,
15+
effect: EffectCallback,
16+
deps?: React.DependencyList,
17+
) => void;
18+
19+
const useInsertionEffectPolyfill: UseCompatibleInsertionEffect = (
20+
renderEffect,
21+
effect,
22+
deps,
23+
) => {
24+
React.useMemo(renderEffect, deps);
25+
useLayoutEffect(effect, deps);
26+
};
27+
28+
const useCompatibleInsertionEffect: UseCompatibleInsertionEffect =
29+
useInsertionEffect
30+
? (renderEffect, effect, deps) =>
31+
useInsertionEffect(() => {
32+
renderEffect();
33+
return effect();
34+
}, deps)
35+
: useInsertionEffectPolyfill;
36+
37+
export default useCompatibleInsertionEffect;

src/hooks/useGlobalCache.tsx

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as React from 'react';
22
import type { KeyType } from '../Cache';
33
import StyleContext from '../StyleContext';
4+
import useCompatibleInsertionEffect from './useCompatibleInsertionEffect';
45
import useHMR from './useHMR';
5-
import useInsertionEffect from './useInsertionEffect';
66

77
export default function useGlobalCache<CacheType>(
88
prefix: string,
@@ -51,28 +51,32 @@ export default function useGlobalCache<CacheType>(
5151
const cacheContent = globalCache.get(fullPath)![1];
5252

5353
// Remove if no need anymore
54-
useInsertionEffect(() => {
55-
onCacheEffect?.(cacheContent);
56-
57-
// It's bad to call build again in effect.
58-
// But we have to do this since StrictMode will call effect twice
59-
// which will clear cache on the first time.
60-
buildCache(([times, cache]) => [times + 1, cache]);
61-
62-
return () => {
63-
globalCache.update(fullPath, (prevCache) => {
64-
const [times = 0, cache] = prevCache || [];
65-
const nextCount = times - 1;
66-
67-
if (nextCount === 0) {
68-
onCacheRemove?.(cache, false);
69-
return null;
70-
}
71-
72-
return [times - 1, cache];
73-
});
74-
};
75-
}, [deps]);
54+
useCompatibleInsertionEffect(
55+
() => {
56+
onCacheEffect?.(cacheContent);
57+
},
58+
() => {
59+
// It's bad to call build again in effect.
60+
// But we have to do this since StrictMode will call effect twice
61+
// which will clear cache on the first time.
62+
buildCache(([times, cache]) => [times + 1, cache]);
63+
64+
return () => {
65+
globalCache.update(fullPath, (prevCache) => {
66+
const [times = 0, cache] = prevCache || [];
67+
const nextCount = times - 1;
68+
69+
if (nextCount === 0) {
70+
onCacheRemove?.(cache, false);
71+
return null;
72+
}
73+
74+
return [times - 1, cache];
75+
});
76+
};
77+
},
78+
[deps],
79+
);
7680

7781
return cacheContent;
7882
}

src/hooks/useInsertionEffect.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/legacy.spec.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { render } from '@testing-library/react';
22
import * as React from 'react';
3+
import { useLayoutEffect } from 'react';
4+
import { expect } from 'vitest';
35
import type { CSSInterpolation } from '../src';
46
import { Theme, useCacheToken, useStyleRegister } from '../src';
57

@@ -50,14 +52,15 @@ describe('legacy React version', () => {
5052

5153
interface BoxProps {
5254
propToken?: DesignToken;
55+
children?: React.ReactNode;
5356
}
5457

55-
const Box = ({ propToken = baseToken }: BoxProps) => {
58+
const Box = ({ propToken = baseToken, children }: BoxProps) => {
5659
const [token] = useCacheToken<DerivativeToken>(theme, [propToken]);
5760

5861
useStyleRegister({ theme, token, path: ['.box'] }, () => [genStyle(token)]);
5962

60-
return <div className="box" />;
63+
return <div className="box">{children}</div>;
6164
};
6265

6366
// We will not remove style immediately,
@@ -106,4 +109,27 @@ describe('legacy React version', () => {
106109

107110
test('StrictMode', (ele) => <React.StrictMode>{ele}</React.StrictMode>);
108111
});
112+
113+
it('should not race with other useLayoutEffect', () => {
114+
let styleCount = 0;
115+
116+
const Child = () => {
117+
useLayoutEffect(() => {
118+
styleCount = document.head.querySelectorAll('style').length;
119+
}, []);
120+
121+
return null;
122+
};
123+
const Demo = () => {
124+
return (
125+
<Box>
126+
<Child />
127+
</Box>
128+
);
129+
};
130+
131+
render(<Demo />);
132+
133+
expect(styleCount).toBe(1);
134+
});
109135
});

0 commit comments

Comments
 (0)