Skip to content

Commit 73244ce

Browse files
committed
feat(components/transition-group): 重构并优化 TransitionGroup 组件,tag 属性可设置为 null 去除包裹元素
1 parent 2f17dce commit 73244ce

File tree

7 files changed

+249
-138
lines changed

7 files changed

+249
-138
lines changed

packages/components/src/transition-group/TransitionGroup.tsx

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { TransitionGroupProps } from './transition-group.types';
22
import { useChildMap, useWrapper, useFlips } from './hooks';
33
import { RequiredPart } from '@tool-pack/types';
4-
import { useForwardRef } from '@pkg/shared';
5-
import React from 'react';
4+
import { forwardRef, memo } from 'react';
5+
import type { FC } from 'react';
66

77
/**
88
* v1 版有部分 bug 不好解决。
@@ -19,23 +19,21 @@ const defaultProps = {
1919
tag: 'div',
2020
} satisfies TransitionGroupProps;
2121

22-
const TransitionGroup: React.FC<TransitionGroupProps> = React.forwardRef<
22+
const TransitionGroup: FC<TransitionGroupProps> = forwardRef<
2323
HTMLDivElement,
2424
TransitionGroupProps
25-
>((props, _ref) => {
25+
>((props, ref) => {
2626
const { children, name, ...rest } = props as RequiredPart<
2727
TransitionGroupProps,
2828
keyof typeof defaultProps
2929
>;
30-
31-
const ref = useForwardRef(_ref);
3230
const childMap = useChildMap(children, name);
3331
const wrapper = useWrapper(childMap, rest, ref);
34-
useFlips(ref, childMap, name);
32+
useFlips(childMap, name);
3533
return wrapper;
3634
});
3735

3836
TransitionGroup.defaultProps = defaultProps;
3937
TransitionGroup.displayName = 'TransitionGroup';
4038

41-
export default React.memo(TransitionGroup);
39+
export default memo(TransitionGroup);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* title: 标签
3+
* description: 默认标签为 div,可设置 tag 为其它标签,当 tag 为 null 时,移除包裹元素
4+
*/
5+
6+
import {
7+
TransitionGroup,
8+
Transition,
9+
Button,
10+
Space,
11+
} from '@tool-pack/react-ui';
12+
import React, { useCallback, useState, useRef } from 'react';
13+
import styles from './list.module.scss';
14+
15+
const App: React.FC = () => {
16+
const [, update] = useState({});
17+
const forceUpdate = useCallback(() => update({}), []);
18+
19+
const children = useRef<number[]>([...Array.from({ length: 10 }).keys()]);
20+
21+
const index = useRef(children.current.length);
22+
function addChild() {
23+
const list = children.current;
24+
const splice = list.splice(~~(Math.random() * list.length), list.length);
25+
list.push(index.current);
26+
list.push(...splice);
27+
forceUpdate();
28+
index.current++;
29+
}
30+
function removeChild(item: number) {
31+
const index = children.current.indexOf(item);
32+
if (index === -1) return;
33+
children.current.splice(index, 1);
34+
forceUpdate();
35+
}
36+
function removeRandomChild() {
37+
removeChild(children.current[~~(Math.random() * children.current.length)]!);
38+
}
39+
40+
return (
41+
<div className={styles['root']}>
42+
<Space style={{ justifyContent: 'center' }}>
43+
<Button onClick={addChild} type="primary">
44+
添加
45+
</Button>
46+
<Button onClick={removeRandomChild} type="warning" plain>
47+
移除
48+
</Button>
49+
</Space>
50+
<br />
51+
<div className="group-container">
52+
<TransitionGroup name="group" tag={null}>
53+
{children.current.map((item) => {
54+
return (
55+
<Transition key={item}>
56+
<div>{item}</div>
57+
</Transition>
58+
);
59+
})}
60+
</TransitionGroup>
61+
</div>
62+
</div>
63+
);
64+
};
65+
66+
export default App;
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import {
2+
isValidElement,
3+
cloneElement,
4+
useEffect,
5+
useState,
6+
Children,
7+
} from 'react';
18
import { type TransitionProps, transitionCBAdapter } from '@pkg/components';
2-
import type { ChildMap } from '../transition-group.types';
3-
import React, { useEffect, useState } from 'react';
4-
import { useIsInitDep } from '@pkg/shared';
5-
export function useChildMap(children: React.ReactNode, name: string) {
9+
import type { RefAttributes, ReactElement, ReactNode, Key } from 'react';
10+
import type { ChildMapValue, ChildMap } from '../transition-group.types';
11+
import { useIsInitDep, forwardRefs } from '@pkg/shared';
12+
13+
export function useChildMap(children: ReactNode, name: string): ChildMap {
614
const isInit = useIsInitDep(children);
715

816
const [childMap, setChildMap] = useState((): ChildMap => {
@@ -16,7 +24,7 @@ export function useChildMap(children: React.ReactNode, name: string) {
1624
});
1725
});
1826

19-
const onLeaved = (key: React.Key) => {
27+
const onLeaved = (key: Key) => {
2028
// leave 可能会丢失
2129
setChildMap((prevChildren) => {
2230
const map = new Map(prevChildren);
@@ -34,66 +42,28 @@ export function useChildMap(children: React.ReactNode, name: string) {
3442
}
3543

3644
const nextChildMap = (
37-
children: React.ReactNode,
45+
children: ReactNode,
3846
prevChildMap: ChildMap,
3947
name: string,
40-
onLeaved: (key: React.Key) => void,
48+
onLeaved: (key: Key) => void,
4149
): ChildMap => {
4250
const childMap = createMap(children);
43-
return mergeMaps(prevChildMap, childMap, (child, key): React.ReactNode => {
44-
if (!React.isValidElement(child)) return child;
45-
46-
const inNext = childMap.get(key) !== undefined;
47-
const inPrev = prevChildMap.get(key) !== undefined;
48-
const inBoth = inNext && inPrev;
49-
50-
const isAdd = inNext && !inPrev;
51-
const isRemove = !inNext && inPrev;
52-
53-
if (isRemove) {
54-
if (child.props.show === false) return child;
55-
// 因为 remove 了的 child 是不存在于 next 的,所以这个 child 是旧的,是 clone 过的
56-
// tips: 加了 on 就不会等待多个 remove 完才 move,而是 remove 一个 move 一个
57-
return cloneTransition(child, { appear: false, show: false });
58-
}
59-
60-
const on = transitionCBAdapter({
61-
onAfterLeave: () => onLeaved(child.key || ''),
62-
});
63-
64-
if (isAdd) {
65-
// 旧的不存在,所以 child 是新创建的,是未 clone 过的
66-
return cloneTransition(child, {
67-
appear: true,
68-
name: name,
69-
show: true,
70-
on,
71-
});
72-
}
73-
74-
if (inBoth) {
75-
// 两者皆有取最新,所以 child 是新创建的,是未 clone 过的
76-
return cloneTransition(child, {
77-
appear: false,
78-
show: true,
79-
name: name,
80-
on,
81-
});
82-
}
83-
return child;
84-
});
51+
return mergeMaps(prevChildMap, childMap, name, onLeaved);
8552
};
8653

8754
function createMap(
88-
children: React.ReactNode,
89-
callback: (child: React.ReactElement) => React.ReactNode = (v) => v,
55+
children: ReactNode,
56+
callback: (child: ReactElement) => ChildMapValue = (v) => ({
57+
reactEl: v,
58+
ref: null,
59+
}),
9060
): ChildMap {
9161
const map: ChildMap = new Map();
9262
if (!children) return map;
9363

94-
// 如果没有手动添加key, React.Children.map会自动添加key
95-
React.Children.map(children, (c) => c)?.forEach((child) => {
96-
if (!React.isValidElement(child)) return;
64+
// 如果没有手动添加key, Children.map会自动添加key
65+
Children.map(children, (c) => c)?.forEach((child) => {
66+
if (!isValidElement(child)) return;
9767
const key = child.key || '';
9868
if (!key) return;
9969
map.set(key, callback(child));
@@ -105,14 +75,14 @@ function createMap(
10575
function mergeMaps(
10676
prevMap: ChildMap,
10777
nextMap: ChildMap,
108-
callback: (child: React.ReactNode, key: React.Key) => React.ReactNode,
78+
name: string,
79+
onLeaved: (key: Key) => void,
10980
): ChildMap {
110-
const getValue = (key: React.Key) => nextMap.get(key) ?? prevMap.get(key);
111-
112-
let insertKeys: React.Key[] = [];
113-
const insertKeysMap = new Map<React.Key, typeof insertKeys>();
81+
let insertKeys: Key[] = [];
82+
const insertKeysMap = new Map<Key, typeof insertKeys>();
83+
const result: ChildMap = new Map();
11484

115-
prevMap.forEach((_, key) => {
85+
prevMap.forEach((_, key): void => {
11686
if (nextMap.has(key)) {
11787
if (!insertKeys.length) return;
11888
insertKeysMap.set(key, insertKeys);
@@ -121,23 +91,95 @@ function mergeMaps(
12191
}
12292
insertKeys.push(key);
12393
});
124-
125-
const result: ChildMap = new Map();
126-
const push = (k: React.Key) => result.set(k, callback(getValue(k), k));
127-
nextMap.forEach((_, key) => {
94+
nextMap.forEach((_, key): void => {
12895
const keys = insertKeysMap.get(key);
12996
if (keys) keys.forEach(push);
13097
push(key);
13198
});
13299
insertKeys.forEach(push);
133100

134101
return result;
102+
103+
function push(k: Key): void {
104+
const value = mergeMapValue(
105+
prevMap.get(k),
106+
nextMap.get(k),
107+
k,
108+
name,
109+
onLeaved,
110+
);
111+
value && result.set(k, value);
112+
}
113+
}
114+
115+
function mergeMapValue(
116+
prevValue: ChildMapValue | undefined,
117+
nextValue: ChildMapValue | undefined,
118+
key: Key,
119+
name: string,
120+
onLeaved: (key: Key) => void,
121+
): ChildMapValue | void {
122+
const inNext = nextValue?.reactEl !== undefined;
123+
const inPrev = prevValue?.reactEl !== undefined;
124+
const inBoth = inNext && inPrev;
125+
126+
const isAdd = inNext && !inPrev;
127+
const isRemove = !inNext && inPrev;
128+
129+
if (isRemove) {
130+
// noinspection PointlessBooleanExpressionJS
131+
if (
132+
(prevValue.reactEl as ReactElement<TransitionProps>).props.show === false
133+
)
134+
return prevValue;
135+
// 因为 remove 了的 child 是不存在于 next 的,所以这个 child 是旧的,是 clone 过的
136+
// tips: 加了 on 就不会等待多个 remove 完才 move,而是 remove 一个 move 一个
137+
return cloneTransition(
138+
prevValue.reactEl,
139+
{ appear: false, show: false },
140+
prevValue.ref,
141+
);
142+
}
143+
144+
const on = transitionCBAdapter({
145+
onAfterLeave: () => onLeaved(key || ''),
146+
});
147+
148+
if (isAdd) {
149+
// 旧的不存在,所以 child 是新创建的,是未 clone 过的
150+
return cloneTransition(
151+
nextValue.reactEl,
152+
{
153+
appear: true,
154+
name: name,
155+
show: true,
156+
on,
157+
},
158+
prevValue?.ref,
159+
);
160+
}
161+
162+
if (inBoth) {
163+
// 两者皆有取最新,所以 child 是新创建的,是未 clone 过的
164+
return cloneTransition(
165+
nextValue.reactEl,
166+
{
167+
appear: false,
168+
show: true,
169+
name: name,
170+
on,
171+
},
172+
prevValue.ref,
173+
);
174+
}
135175
}
136176

137177
function cloneTransition(
138-
transition: React.ReactElement,
178+
transition: ReactElement,
139179
props: Partial<TransitionProps>,
140-
) {
180+
ref: HTMLElement | null = null,
181+
): ChildMapValue {
182+
const result: ChildMapValue = { reactEl: transition, ref };
141183
const _props = { ...props } as TransitionProps;
142184
const nextOn = props.on;
143185
if (nextOn) {
@@ -147,5 +189,23 @@ function cloneTransition(
147189
nextOn(el, status, lifeCircle);
148190
};
149191
}
150-
return React.cloneElement<TransitionProps>(transition, _props);
192+
193+
result.reactEl = cloneElement<TransitionProps>(
194+
transition,
195+
_props,
196+
cloneChildren(),
197+
);
198+
return result;
199+
200+
function cloneChildren() {
201+
const children = transition.props.children;
202+
return isValidElement(children)
203+
? cloneElement(children as ReactElement, {
204+
ref: (el: HTMLElement) => {
205+
forwardRefs(el, (children as RefAttributes<any>).ref);
206+
el && (result.ref = el);
207+
},
208+
})
209+
: undefined;
210+
}
151211
}

0 commit comments

Comments
 (0)