Skip to content

Commit 58a8dd6

Browse files
committed
feat(shared/hooks): 新增 useChildrenWithRefs hook
1 parent fde7561 commit 58a8dd6

File tree

3 files changed

+105
-0
lines changed

3 files changed

+105
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { cloneElement, ReactNode, useEffect, useState, Children } from 'react';
2+
import { renderHook, render, act } from '@testing-library/react';
3+
import { useChildrenWithRefs } from '@pkg/shared';
4+
5+
describe('useChildrenWithRefs', () => {
6+
test('base', () => {
7+
const hook = renderHook(() => {
8+
const [children, setChildren] = useState<ReactNode>();
9+
const hook = useChildrenWithRefs(children);
10+
return [hook, setChildren] as const;
11+
});
12+
13+
expect(hook.result.current[0]).toEqual([undefined, []]);
14+
});
15+
test('react ref', () => {
16+
const ref = jest.fn();
17+
const App = () => {
18+
return Children.map(
19+
<div className="test" draggable={true} id="div">
20+
1
21+
</div>,
22+
(child) => cloneElement(child, { ref }),
23+
);
24+
};
25+
const { container } = render(<App />);
26+
expect(ref).toHaveBeenCalled();
27+
expect(container.firstChild).toHaveClass('test');
28+
expect(container.firstChild).toHaveAttribute('draggable');
29+
expect(ref.mock.calls[0][0]).toBe(container.firstChild);
30+
});
31+
test('app', () => {
32+
const _refs: HTMLElement[] = [];
33+
const App = () => {
34+
const [children, setChildren] = useState<ReactNode>(
35+
<div className="foo">foo</div>,
36+
);
37+
const [newChildren, refs] = useChildrenWithRefs(children);
38+
39+
useEffect(() => {
40+
_refs.push(...refs);
41+
}, [refs]);
42+
43+
return (
44+
<>
45+
{newChildren}
46+
<button onClick={() => setChildren(<div className="bar">bar</div>)}>
47+
切换 children
48+
</button>
49+
</>
50+
);
51+
};
52+
53+
expect(_refs).toEqual([]);
54+
const {
55+
container: { firstChild },
56+
} = render(<App />);
57+
58+
expect(firstChild).toHaveTextContent('foo');
59+
expect(firstChild).toHaveClass('foo');
60+
expect(_refs.length).toBe(1);
61+
expect(_refs[0]).toBe(firstChild);
62+
63+
_refs.length = 0;
64+
act(() => document.querySelector('button')!.click());
65+
expect(firstChild).toHaveTextContent('bar');
66+
expect(firstChild).toHaveClass('bar');
67+
expect(_refs.length).toBe(1);
68+
expect(_refs[0]).toBe(firstChild);
69+
});
70+
});

packages/shared/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export * from './useStateRef';
2121
export * from './useFollowingState';
2222
export * from './useOnlineStatus';
2323
export * from './useOldValue';
24+
export * from './useChildrenWithRefs';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
isValidElement,
3+
cloneElement,
4+
ReactElement,
5+
ReactNode,
6+
Children,
7+
useMemo,
8+
} from 'react';
9+
10+
/**
11+
* 对 children 所有有效元素添加 ref,并且连通原来的 ref,返回 children 与 refs
12+
*/
13+
export function useChildrenWithRefs(
14+
children?: ReactNode,
15+
): [children: ReactNode, refs: HTMLElement[]] {
16+
return useMemo(() => {
17+
const refs: HTMLElement[] = [];
18+
const newChildren = Children.map(children, (child, index) => {
19+
if (!isValidElement(child)) return child;
20+
return cloneElement(child as ReactElement, {
21+
ref: (el: HTMLElement) => {
22+
refs[index] = el;
23+
const originRef = child.props.ref;
24+
25+
if (!originRef) return;
26+
// 连通原来的 ref
27+
if (typeof originRef === 'function') originRef(el);
28+
else originRef.current = el;
29+
},
30+
});
31+
});
32+
return [newChildren, refs];
33+
}, [children]);
34+
}

0 commit comments

Comments
 (0)