一些常用的react组件封装实现示例--1
这些组件的实现可以帮助那些想要学习 react 并熟练 react 语法的人学习。
手风琴组件
实现一个具有多个可折叠内容元素的手风琴菜单,实现思路如下:
- 定义一个 AccordionItem 组件,它呈现一个
<div>
。 按钮更新组件并通过 handleClick 回调通知其父级。 - 使用 AccordionItem 中的 isCollapsed 属性确定其外观并设置其类名。
- 定义 Accordion 组件。 使用 useState() 挂钩将 bindIndex 状态变量的值初始化为 defaultIndex。
- 通过识别函数的名称过滤子项以删除除 AccordionItem 之外的不必要节点。
- 在收集的节点上使用 Array.prototype.map() 来呈现各个可折叠元素。
- 定义 changeItem,当点击 AccordionItem 的
<div>
时执行。 - changeItem 执行传递的回调 onItemClick,并根据单击的元素更新 bindIndex。
tsx 代码如下所示:
tsx
import { cx, css } from '@emotion/css';
import React, { useState } from 'react';
import type { ReactNode, SyntheticEvent } from 'react';
const baseStyle = css`
line-height: 1.5715;
`;
const AccordionContainer = cx(
baseStyle,
css`
box-sizing: border-box;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
background-color: #fafafa;
border: 1px solid #d9d9d9;
border-bottom: 0;
border-radius: 2px;
`
);
const AccordionItemContainer = css`
border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
baseStyle,
css`
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
padding: 12px 16px;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
transition: all 0.3s, visibility 0s;
box-sizing: border-box;
`
);
const AccordionItemContent = css`
color: #000000d9;
background-color: #fff;
border-top: 1px solid #d9d9d9;
transition: all 0.3s ease-in-out;
padding: 16px;
&.collapsed {
display: none;
}
&.expanded {
display: block;
}
`;
interface AccordionItemType {
index: string | number;
label: string;
isCollapsed: boolean;
handleClick(e: SyntheticEvent): void;
children: ReactNode;
}
interface AccordionType {
defaultIndex: number | string;
onItemClick(key: number | string): void;
children: JSX.Element[];
}
const AccordionItem = (props: Partial<AccordionItemType>) => {
const { label, isCollapsed, handleClick, children } = props;
return (
<div className={AccordionItemContainer} onClick={handleClick}>
<div className={AccordionItemHeader}>{label}</div>
<div
aria-expanded={isCollapsed}
className={`${AccordionItemContent}${
isCollapsed ? ' collapsed' : ' expanded'
}`}
>
{children}
</div>
</div>
);
};
// 设置一个函数名用作判断,生产环境无法使用function.name
AccordionItem.displayName = 'AccordionItem';
const Accordion = (props: Partial<AccordionType>) => {
const { defaultIndex, onItemClick, children } = props;
const [bindIndex, setBindIndex] = useState(defaultIndex);
const changeItem = (index: number | string) => {
if (typeof onItemClick === 'function') {
onItemClick(index);
}
if (index !== bindIndex) {
setBindIndex(index);
}
};
const items = children?.filter(
item => item?.type?.displayName === 'AccordionItem'
);
return (
<div className={AccordionContainer}>
{items?.map(({ props: { index, label, children } }) => (
<AccordionItem
key={index}
label={label}
children={children}
isCollapsed={bindIndex !== index}
handleClick={() => changeItem(index)}
/>
))}
</div>
);
};
Accordion.AccordionItem = AccordionItem;
export default Accordion;
jsx 代码如下所示:
jsx
import { cx, css } from '@emotion/css';
import React, { useState } from 'react';
const baseStyle = css`
line-height: 1.5715;
`;
const AccordionContainer = cx(
baseStyle,
css`
box-sizing: border-box;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
background-color: #fafafa;
border: 1px solid #d9d9d9;
border-bottom: 0;
border-radius: 2px;
`
);
const AccordionItemContainer = css`
border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
baseStyle,
css`
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
padding: 12px 16px;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
transition: all 0.3s, visibility 0s;
box-sizing: border-box;
`
);
const AccordionItemContent = css`
color: #000000d9;
background-color: #fff;
border-top: 1px solid #d9d9d9;
transition: all 0.3s ease-in-out;
padding: 16px;
&.collapsed {
display: none;
}
&.expanded {
display: block;
}
`;
const AccordionItem = props => {
const { label, isCollapsed, handleClick, children } = props;
return (
<div className={AccordionItemContainer} onClick={handleClick}>
<div className={AccordionItemHeader}>{label}</div>
<div
aria-expanded={isCollapsed}
className={`${AccordionItemContent}${
isCollapsed ? ' collapsed' : ' expanded'
}`}
>
{children}
</div>
</div>
);
};
// 设置一个函数名用作判断,生产环境无法使用function.name
AccordionItem.displayName = 'AccordionItem';
const Accordion = props => {
const { defaultIndex, onItemClick, children } = props;
const [bindIndex, setBindIndex] = useState(defaultIndex);
const changeItem = index => {
if (typeof onItemClick === 'function') {
onItemClick(index);
}
if (index !== bindIndex) {
setBindIndex(index);
}
};
const items = children?.filter(
item => item?.type?.displayName === 'AccordionItem'
);
return (
<div className={AccordionContainer}>
{items?.map(({ props: { index, label, children } }) => (
<AccordionItem
key={index}
label={label}
children={children}
isCollapsed={bindIndex !== index}
handleClick={() => changeItem(index)}
/>
))}
</div>
);
};
Accordion.AccordionItem = AccordionItem;
export default Accordion;
参考在线示例。
特别说明: 该组件的实现依赖@emotion 和 react。
提示组件
实现一个带有 prop 类型的提示组件,实现思路如下:
- 使用 useState() 钩子创建 isShown 和 isLeaving 状态变量,并最初将两者都设置为 false。
- 定义 timeoutId 以保留计时器实例以在组件卸载时清除。
- 卸载组件时,使用 useEffect() 钩子将 isShown 的值更新为 true,并使用 timeoutId 清除间隔。
- 定义一个 closeAlert 函数,通过显示淡出动画将组件设置为从 DOM 中移除,并通过 setTimeout() 将 isShown 设置为 false。
less 代码如下:
less
@keyframes leave {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@baseSelector: alert;
@infoBgColor: #e6f7ff;
@infoBorderColor: #91d5ff;
@warningBgColor: #fffbe6;
@warningBorderColor: #ffe58f;
@errorBgColor: #fff2f0;
@errorBorderColor: #ffccc7;
@successBgColor: #f6ffed;
@successBorderColor: #b7eb8f;
@color: rgba(0, 0, 0, 0.85);
@closeActiveColor: fadeout(@color, 25%);
.@{baseSelector} {
box-sizing: border-box;
margin: 0;
color: @color;
font-size: 14px;
line-height: 1.5715;
list-style: none;
position: relative;
display: flex;
align-items: center;
padding: 8px 15px;
word-wrap: break-word;
border-radius: 2px;
border: 1px solid transparent;
&.@{baseSelector}-block {
width: percentage(1);
}
&.@{baseSelector}-info {
background-color: @infoBgColor;
border-color: @infoBorderColor;
}
&.@{baseSelector}-warning {
background-color: @warningBgColor;
border-color: @warningBorderColor;
}
&.@{baseSelector}-error {
background-color: @errorBgColor;
border-color: @errorBorderColor;
}
&.@{baseSelector}-success {
background-color: @successBgColor;
border-color: @successBorderColor;
}
.@{baseSelector}-close {
margin-left: 8px;
padding: 0;
overflow: hidden;
font-size: 16px;
line-height: 12px;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
margin: 0;
color: @color;
position: absolute;
right: 10px;
top: 10px;
&:hover {
color: @closeActiveColor;
}
}
}
tsx 代码如下:
tsx
import React, { useState, useEffect } from 'react';
import './alert.less';
interface AlertPropType {
isDefaultShown: boolean;
timeout: number;
type: string;
message: string;
showClose: boolean;
block: boolean;
}
const Alert = (props: Partial<AlertPropType>) => {
const {
isDefaultShown,
timeout = 250,
type,
message,
showClose,
block
} = props;
const [isShown, setIsShown] = useState(isDefaultShown);
const [isLeaving, setIsLeaving] = useState(false);
let timer: number | null = null;
useEffect(() => {
setIsShown(true);
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [timeout, timer, isDefaultShown]);
const closeAlert = () => {
setIsLeaving(true);
timer = setTimeout(() => {
setIsLeaving(false);
setIsShown(false);
}, timeout);
};
return isShown ? (
<div
className={`alert ${'alert-' + type}${isLeaving ? ' leaving' : ''}${
block ? ' alert-block' : ''
}`}
role="alert"
>
<button
className="alert-close"
onClick={closeAlert}
style={{ display: showClose ? 'block' : 'none' }}
>
×
</button>
{message}
</div>
) : null;
};
export default Alert;
jsx 代码如下:
jsx
import React, { useState, useEffect } from 'react';
import './alert.less';
const Alert = props => {
const {
isDefaultShown,
timeout = 250,
type,
message,
showClose,
block
} = props;
const [isShown, setIsShown] = useState(isDefaultShown);
const [isLeaving, setIsLeaving] = useState(false);
let timer = null;
useEffect(() => {
setIsShown(true);
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [timeout, timer, isDefaultShown]);
const closeAlert = () => {
setIsLeaving(true);
timer = setTimeout(() => {
setIsLeaving(false);
setIsShown(false);
}, timeout);
};
return isShown ? (
<div
className={`alert ${'alert-' + type}${isLeaving ? ' leaving' : ''}${
block ? ' alert-block' : ''
}`}
role="alert"
>
<button
className="alert-close"
onClick={closeAlert}
style={{ display: showClose ? 'block' : 'none' }}
>
×
</button>
{message}
</div>
) : null;
};
export default Alert;
参考在线示例。
特别说明: 该组件的实现依赖 react 和 less。
自动文本链接组件
实现一个将字符串呈现为纯文本,并将 URL 转换为适当的链接元素的组件,实现思路如下所示:
- 使用带有正则表达式的 String.prototype.split() 和 String.prototype.match() 来查找字符串中的 URL。
- 返回呈现为
<a>
元素的匹配 URL,如有必要,处理缺少的协议前缀。 - 将字符串的其余部分呈现为纯文本。
tsx 代码如下所示:
tsx
import React from 'react';
import styled from '@emotion/styled';
const LinkContainer = styled.span`
display: inline-block;
a {
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
color: #1890ff;
background: transparent;
text-decoration: none;
&:hover {
color: #40a9ff;
}
}
`;
interface AutoLinkPropType {
text: string;
}
const AutoLink = (props: Partial<AutoLinkPropType>) => {
const { text } = props;
const delimiter =
/((?:https?:\/\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\-]{1,61}[a-z0-9])?\.[^\.|\s])+[a-z\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\d{1,5})*[a-z0-9.,_\/~#&=;%+?\-\\(\\)]*)/gi;
return (
<LinkContainer>
{text?.split(delimiter).map(word => {
const match = word.match(delimiter);
if (match) {
const url = match[0];
return (
<a
href={url.startsWith('http') ? url : `http://${url}`}
target="_blank"
rel="noopener noreferrer"
>
{url}
</a>
);
}
return word;
})}
</LinkContainer>
);
};
export default AutoLink;
jsx 代码如下所示:
jsx
import React from 'react';
import styled from '@emotion/styled';
const LinkContainer = styled.span`
display: inline-block;
a {
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
color: #1890ff;
background: transparent;
text-decoration: none;
&:hover {
color: #40a9ff;
}
}
`;
const AutoLink = props => {
const { text } = props;
const delimiter =
/((?:https?:\/\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\-]{1,61}[a-z0-9])?\.[^\.|\s])+[a-z\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\d{1,5})*[a-z0-9.,_\/~#&=;%+?\-\\(\\)]*)/gi;
return (
<LinkContainer>
{text?.split(delimiter).map(word => {
const match = word.match(delimiter);
if (match) {
const url = match[0];
return (
<a
href={url.startsWith('http') ? url : `http://${url}`}
target="_blank"
rel="noopener noreferrer"
>
{url}
</a>
);
}
return word;
})}
</LinkContainer>
);
};
export default AutoLink;
参考在线示例。
特别说明: 该组件的实现依赖@emotion 和 react。
受控的输入框组件
实现一个受控的 <input>
元素,该元素使用回调函数通知其父级有关值的更新,实现思路如下所示:
- 使用从父级传下来的值作为受控输入字段的值。
- 使用 onChange 事件触发 onChange 回调并将新值发送给父级。
- 父级必须更新输入字段的 value 属性,以便其值在用户输入时更改。
tsx 代码如下所示:
tsx
import styled from '@emotion/styled';
import React from 'react';
import type { SyntheticEvent } from 'react';
const StyleInput = styled.input`
box-sizing: border-box;
margin: 0;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: 'tnum';
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
padding: 4px 11px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 2px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
border-right-width: 1px;
outline: 0;
}
`;
type LiteralUnion<T extends U, U> = T & (U & {});
interface ControlledInputProps {
type: LiteralUnion<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week',
string
>;
value: string;
onChange(v: string): void;
placeholder: string;
}
const ControlledInput = (props: Partial<ControlledInputProps>) => {
const { value, onChange, ...rest } = props;
const onChangeHandler = (e: SyntheticEvent) => {
if (onChange) {
onChange((e.target as HTMLInputElement).value);
}
};
return (
<StyleInput value={value} onChange={onChangeHandler} {...rest}></StyleInput>
);
};
jsx 代码如下所示:
jsx
import styled from '@emotion/styled';
import React from 'react';
const StyleInput = styled.input`
box-sizing: border-box;
margin: 0;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: 'tnum';
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
padding: 4px 11px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 2px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
border-right-width: 1px;
outline: 0;
}
`;
const ControlledInput = props => {
const { value, onChange, ...rest } = props;
const onChangeHandler = e => {
onChange?.(e.target.value);
};
return (
<StyleInput value={value} onChange={onChangeHandler} {...rest}></StyleInput>
);
};
export default ControlledInput;
参考在线示例。
特别说明: 该组件的实现依赖@emotion 和 react。
倒计时组件
实现一个倒计时组件,实现思路如下所示:
- 使用 useState() 钩子创建一个状态变量来保存时间值。 从 props 初始化它并将其解构为它的组件。
- 使用 useState() 钩子创建暂停和结束状态变量,用于防止计时器在暂停或时间用完时滴答作响。
- 创建一个方法 tick,它根据当前值更新时间值(即将时间减少一秒)。
- 创建一个方法 reset,将所有状态变量重置为其初始状态。
- 使用 useEffect() 钩子通过 setTimeout()结合递归 每秒调用一次 tick 方法,并在组件卸载时使用 clearTimeout() 进行清理。
- 使用 String.prototype.padStart() 将时间数组的每个部分填充为两个字符,以创建计时器的可视化表示。
helper.ts 代码如下所示:
ts
import type { MutableRefObject, DependencyList, Ref } from 'react';
import { useImperativeHandle, useEffect, useRef } from 'react';
export const useChildrenHandler = <T, K extends object>(
originRef: MutableRefObject<T>,
handler: K,
deps?: DependencyList
): void =>
useImperativeHandle(
originRef,
() => {
return {
...originRef.current,
...handler
};
},
deps
);
export type ImperFunc = (...args: any[]) => any;
export type ImperRef = Record<string, ImperFunc>;
export type ImperItem = {
ref: Ref<ImperRef>;
};
let timer: number;
export const useTimeout = (callback, delay = 1000) => {
const ref = useRef() as MutableRefObject<() => void>;
useEffect(() => {
ref.current = callback;
});
useEffect(() => {
const handler = () => {
ref.current();
timer = setTimeout(handler, delay);
};
handler();
return () => clearTimeout(timer);
}, [delay]);
};
CountDown.tsx 代码如下所示:
tsx
import React, { forwardRef, useState, useCallback } from 'react';
import type { MutableRefObject } from 'react';
import styled from '@emotion/styled';
import type { ImperItem, ImperRef } from './helper';
import { useChildrenHandler, useTimeout } from './helper';
interface CountDownProps {
hours: number;
minutes: number;
seconds: number;
overText?: string;
pausedText?: string;
delimiter?: string;
}
const StyleCountdown = styled.div`
color: rgba(0, 0, 0.85);
margin-bottom: 10px;
font-size: 16px;
`;
const CountDown = forwardRef(
(props: Partial<CountDownProps>, ref: ImperItem['ref']) => {
const {
hours,
minutes,
seconds,
overText = 'Time is up!',
pausedText = 'paused!',
delimiter = ':'
} = props;
const [paused, setPaused] = useState(false);
const [over, setOver] = useState(false);
const [[h = 0, m = 0, s = 0], setTime] = useState([
hours,
minutes,
seconds
]);
const tick = () => {
if (paused || over) {
return;
}
let newH = h,
newM = m,
newS = s;
if (h === 0 && m === 0 && s === 0) {
setOver(true);
}
if (m === 0 && s === 0) {
newH--;
newM = 59;
newS = 59;
}
if (s === 0) {
newM--;
newS = 60;
}
newS--;
setTime([newH, newM, newS]);
};
useTimeout(tick, 1000);
useChildrenHandler(ref as MutableRefObject<ImperRef>, {
onPaused: useCallback((status: boolean) => {
setPaused(!status);
}, []),
onOver: useCallback(() => {
setPaused(false);
setOver(true);
}, []),
onRestart: useCallback(() => {
setTime([h, m, s]);
setPaused(false);
setOver(false);
}, [])
});
const fillZero = (n: number) => n.toString().padStart(2, '0');
return (
<StyleCountdown>
{over
? overText
: paused
? pausedText
: `${fillZero(h)}${delimiter}${fillZero(m)}${delimiter}${fillZero(
s
)}`}
</StyleCountdown>
);
}
);
export default CountDown;
help.js 代码如下所示:
js
import { useImperativeHandle, useEffect, useRef } from 'react';
export const useChildrenHandler = (originRef, handler, deps?) =>
useImperativeHandle(
originRef,
() => ({
...originRef.current,
...handler
}),
deps
);
let timer;
// see this:https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/
export const useTimeout = (callback, delay = 1000) => {
const ref = useRef();
useEffect(() => {
ref.current = callback;
});
useEffect(() => {
const handler = () => {
ref.current();
timer = setTimeout(handler, delay);
};
handler();
return () => clearTimeout(timer);
}, [delay]);
};
CountDown.jsx 代码如下所示:
jsx
import React, { forwardRef, useState, useCallback } from 'react';
import styled from '@emotion/styled';
import { useChildrenHandler, useTimeout } from './helper';
const StyleCountdown = styled.div`
color: rgba(0, 0, 0.85);
margin-bottom: 10px;
font-size: 16px;
`;
const CountDown = forwardRef((props, ref) => {
const {
hours,
minutes,
seconds,
overText = 'Time is up!',
pausedText = 'paused!',
delimiter = ':'
} = props;
const [paused, setPaused] = useState(false);
const [over, setOver] = useState(false);
const [[h = 0, m = 0, s = 0], setTime] = useState([hours, minutes, seconds]);
const tick = () => {
if (paused || over) {
return;
}
let newH = h,
newM = m,
newS = s;
if (h === 0 && m === 0 && s === 0) {
setOver(true);
}
if (m === 0 && s === 0) {
newH--;
newM = 59;
newS = 59;
}
if (s === 0) {
newM--;
newS = 60;
}
newS--;
setTime([newH, newM, newS]);
};
useTimeout(tick, 1000);
useChildrenHandler(ref, {
onPaused: useCallback(status => {
setPaused(!status);
}, []),
onOver: useCallback(() => {
setPaused(false);
setOver(true);
}, []),
onRestart: useCallback(() => {
setTime([h, m, s]);
setPaused(false);
setOver(false);
}, [])
});
const fillZero = n => n.toString().padStart(2, '0');
return (
<StyleCountdown>
{over
? overText
: paused
? pausedText
: `${fillZero(h)}${delimiter}${fillZero(m)}${delimiter}${fillZero(s)}`}
</StyleCountdown>
);
});
export default CountDown;
参考在线示例。
特别说明: 该组件的实现依赖@emotion 和 react。
最后
更多实现参考源码。