前端 React 基础 UI 组件源码级深度剖析
本人掘金号,欢迎点击关注:掘金号地址
本人公众号,欢迎点击关注:公众号地址
一、引言
在前端开发领域,React 作为一个强大的 JavaScript 库,已经成为构建用户界面的首选框架之一。它的核心优势在于组件化开发,允许开发者将复杂的界面拆分成多个小的、可复用的组件。基础 UI 组件是构建 React 应用的基石,它们包括按钮、输入框、下拉菜单等常见元素。深入理解这些基础 UI 组件的源码实现原理,对于开发者提升技术水平、优化组件性能以及进行定制开发都具有重要意义。本文将从源码级别对 React 基础 UI 组件进行全面分析。
二、按钮组件(Button)
2.1 按钮组件的基本实现
按钮是最常见的 UI 组件之一,用于触发各种操作。以下是一个简单的 React 按钮组件的实现:
jsx
// 导入 React 库,用于创建 React 组件
import React from 'react';
// 定义 Button 组件,它是一个函数式组件
const Button = (props) => {
// 从 props 中解构出需要的属性
const {
children, // 按钮内显示的内容,通常是文本
onClick, // 按钮点击事件的回调函数
className // 用于自定义按钮样式的类名
} = props;
// 返回一个 button 元素,将传入的属性和事件绑定到该元素上
return (
<button
onClick={onClick} // 绑定点击事件处理函数
className={className} // 应用自定义类名
>
{children} // 渲染按钮内的内容
</button>
);
};
// 导出 Button 组件,以便在其他文件中使用
export default Button;
在这个简单的按钮组件中,我们接收了三个属性:children 表示按钮内部的文本或其他元素,onClick 是按钮被点击时触发的回调函数,className 用于自定义按钮的样式。通过这种方式,我们可以创建一个基本的可复用按钮组件。
2.2 按钮组件的样式定制
为了让按钮组件更加美观和符合设计要求,我们通常需要对其样式进行定制。以下是一个使用 CSS 模块为按钮添加样式的示例:
jsx
// 导入 React 库
import React from 'react';
// 导入 CSS 模块,用于管理按钮的样式
import styles from './Button.module.css';
// 定义 Button 组件
const Button = (props) => {
const {
children,
onClick,
className
} = props;
// 合并 CSS 模块中的类名和自定义类名
const combinedClassName = `${styles.button} ${className}`;
return (
<button
onClick={onClick}
className={combinedClassName}
>
{children}
</button>
);
};
export default Button;
css
/* Button.module.css */
.button {
/* 设置按钮的背景颜色 */
background-color: #007bff;
/* 设置按钮的文本颜色 */
color: white;
/* 设置按钮的内边距 */
padding: 10px 20px;
/* 设置按钮的边框 */
border: none;
/* 设置按钮的圆角 */
border-radius: 4px;
/* 设置鼠标悬停时的光标样式 */
cursor: pointer;
}
/* 定义按钮悬停时的样式 */
.button:hover {
background-color: #0056b3;
}
在这个示例中,我们使用了 CSS 模块来管理按钮的样式。通过 styles.button 获取 CSS 模块中的类名,并将其与自定义的 className 合并,这样可以在不影响全局样式的情况下,为按钮添加自定义样式。
2.3 按钮组件的状态管理
按钮组件可能会有不同的状态,例如加载状态、禁用状态等。以下是一个支持加载状态的按钮组件的实现:
jsx
import React, { useState } from 'react';
import styles from './Button.module.css';
const Button = (props) => {
const {
children,
onClick,
className
} = props;
// 使用 useState 钩子来管理按钮的加载状态
const [isLoading, setIsLoading] = useState(false);
const handleClick = () => {
// 设置加载状态为 true
setIsLoading(true);
// 调用传入的点击事件处理函数
onClick();
// 模拟加载完成,这里可以根据实际情况修改
setTimeout(() => {
setIsLoading(false);
}, 2000);
};
const combinedClassName = `${styles.button} ${className}`;
return (
<button
onClick={handleClick}
className={combinedClassName}
// 禁用按钮在加载状态下的点击事件
disabled={isLoading}
>
{isLoading ? 'Loading...' : children}
</button>
);
};
export default Button;
在这个示例中,我们使用 useState 钩子来管理按钮的加载状态。当按钮被点击时,将加载状态设置为 true,并显示 “Loading...” 文本。同时,禁用按钮的点击事件,防止用户重复点击。在模拟的加载完成后,将加载状态设置为 false,恢复按钮的正常状态。
2.4 按钮组件的不同类型实现
除了普通按钮,我们还可能需要实现不同类型的按钮,如提交按钮、链接按钮等。以下是一个支持不同类型的按钮组件的实现:
jsx
import React from 'react';
import styles from './Button.module.css';
const Button = (props) => {
const {
children,
onClick,
className,
type = 'button' // 按钮类型,默认为普通按钮
} = props;
const combinedClassName = `${styles.button} ${className}`;
return (
<button
type={type} // 设置按钮类型
onClick={onClick}
className={combinedClassName}
>
{children}
</button>
);
};
export default Button;
在这个示例中,我们添加了一个 type 属性,用于指定按钮的类型。可以传入 'button'、'submit' 或 'reset' 等值,以实现不同类型的按钮功能。
2.5 按钮组件的尺寸和颜色定制
为了满足不同的设计需求,按钮组件通常需要支持尺寸和颜色的定制。以下是一个支持尺寸和颜色定制的按钮组件的实现:
jsx
import React from 'react';
import styles from './Button.module.css';
const Button = (props) => {
const {
children,
onClick,
className,
size = 'medium', // 按钮尺寸,默认为中等
color = 'primary' // 按钮颜色,默认为主要颜色
} = props;
// 根据尺寸和颜色生成对应的类名
const sizeClass = `button-${size}`;
const colorClass = `button-${color}`;
const combinedClassName = `${styles.button} ${sizeClass} ${colorClass} ${className}`;
return (
<button
onClick={onClick}
className={combinedClassName}
>
{children}
</button>
);
};
export default Button;
css
/* Button.module.css */
.button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button-small {
padding: 5px 10px;
font-size: 12px;
}
.button-large {
padding: 15px 30px;
font-size: 16px;
}
.button-primary {
background-color: #007bff;
}
.button-secondary {
background-color: #6c757d;
}
.button-danger {
background-color: #dc3545;
}
在这个示例中,我们添加了 size 和 color 属性,用于定制按钮的尺寸和颜色。通过生成对应的类名,并将其应用到按钮上,实现了按钮的个性化定制。
三、输入框组件(Input)
3.1 输入框组件的基本实现
输入框是用于接收用户输入的重要组件。以下是一个简单的 React 输入框组件的实现:
jsx
import React from 'react';
const Input = (props) => {
const {
type = 'text', // 输入框的类型,默认为文本输入框
placeholder, // 输入框的占位符文本
value, // 输入框的值
onChange, // 输入框内容变化时的回调函数
className // 自定义的类名
} = props;
return (
<input
// 设置输入框的类型
type={type}
// 设置输入框的占位符文本
placeholder={placeholder}
// 设置输入框的值
value={value}
// 绑定输入框内容变化的回调函数
onChange={onChange}
// 应用自定义类名
className={className}
/>
);
};
export default Input;
在这个输入框组件中,我们接收了几个常见的属性:type 表示输入框的类型,如 text、password 等;placeholder 是输入框的占位符文本;value 是输入框当前的值;onChange 是输入框内容变化时触发的回调函数;className 用于自定义输入框的样式。
3.2 输入框组件的受控与非受控状态
在 React 中,输入框可以分为受控组件和非受控组件。受控组件的 value 属性由 React 组件的状态管理,而非受控组件的 value 属性由 DOM 本身管理。以下是一个受控输入框组件的示例:
jsx
import React, { useState } from 'react';
import Input from './Input';
const ControlledInputExample = () => {
// 使用 useState 钩子来管理输入框的值
const [inputValue, setInputValue] = useState('');
const handleInputChange = (event) => {
// 更新输入框的值
setInputValue(event.target.value);
};
return (
<div>
<Input
value={inputValue}
onChange={handleInputChange}
placeholder="Enter some text"
/>
<p>You entered: {inputValue}</p>
</div>
);
};
export default ControlledInputExample;
在这个示例中,我们使用 useState 钩子来管理输入框的值。当输入框的内容发生变化时,调用 handleInputChange 函数更新 inputValue 状态。这样,输入框的值始终由 React 组件的状态控制。
3.3 输入框组件的验证功能
输入框组件通常需要对用户输入进行验证。以下是一个支持简单验证功能的输入框组件的实现:
jsx
import React, { useState } from 'react';
import Input from './Input';
const ValidatedInput = (props) => {
const {
type = 'text',
placeholder,
value,
onChange,
className,
validationRegex, // 验证规则的正则表达式
errorMessage // 验证失败时显示的错误信息
} = props;
const [isValid, setIsValid] = useState(true);
const handleInputChange = (event) => {
const inputValue = event.target.value;
// 调用传入的 onChange 函数
onChange(event);
// 验证输入值是否符合正则表达式
if (validationRegex) {
setIsValid(validationRegex.test(inputValue));
}
};
return (
<div>
<Input
type={type}
placeholder={placeholder}
value={value}
onChange={handleInputChange}
className={className}
/>
{!isValid && <p className="error">{errorMessage}</p>}
</div>
);
};
export default ValidatedInput;
在这个示例中,我们添加了 validationRegex 和 errorMessage 属性,用于定义验证规则和验证失败时显示的错误信息。当输入框内容变化时,调用 validationRegex.test(inputValue) 方法进行验证,并更新 isValid 状态。如果验证失败,显示错误信息。
3.4 输入框组件的前缀和后缀
有些场景下,输入框可能需要添加前缀或后缀,如货币符号、单位等。以下是一个支持前缀和后缀的输入框组件的实现:
jsx
import React from 'react';
import Input from './Input';
const InputWithPrefixSuffix = (props) => {
const {
type = 'text',
placeholder,
value,
onChange,
className,
prefix, // 输入框的前缀
suffix // 输入框的后缀
} = props;
return (
<div className="input-wrapper">
{prefix && <span className="input-prefix">{prefix}</span>}
<Input
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
className={className}
/>
{suffix && <span className="input-suffix">{suffix}</span>}
</div>
);
};
export default InputWithPrefixSuffix;
css
/* InputWithPrefixSuffix.module.css */
.input-wrapper {
display: flex;
align-items: center;
}
.input-prefix,
.input-suffix {
padding: 0 5px;
color: #666;
}
在这个示例中,我们添加了 prefix 和 suffix 属性,用于指定输入框的前缀和后缀。通过 flex 布局将前缀、输入框和后缀组合在一起,实现了带有前缀和后缀的输入框效果。
3.5 输入框组件的自动完成功能
自动完成功能可以提高用户输入效率。以下是一个简单的支持自动完成功能的输入框组件的实现:
jsx
import React, { useState } from 'react';
import Input from './Input';
const AutocompleteInput = (props) => {
const {
type = 'text',
placeholder,
value,
onChange,
className,
options // 自动完成的选项列表
} = props;
const [showOptions, setShowOptions] = useState(false);
const [filteredOptions, setFilteredOptions] = useState(options);
const handleInputChange = (event) => {
const inputValue = event.target.value;
onChange(event);
// 根据输入值过滤选项列表
const filtered = options.filter(option => option.toLowerCase().includes(inputValue.toLowerCase()));
setFilteredOptions(filtered);
setShowOptions(inputValue.length > 0);
};
const handleOptionSelect = (option) => {
onChange({ target: { value: option } });
setShowOptions(false);
};
return (
<div className="autocomplete-wrapper">
<Input
type={type}
placeholder={placeholder}
value={value}
onChange={handleInputChange}
className={className}
/>
{showOptions && (
<ul className="autocomplete-options">
{filteredOptions.map(option => (
<li key={option} onClick={() => handleOptionSelect(option)}>
{option}
</li>
))}
</ul>
)}
</div>
);
};
export default AutocompleteInput;
css
/* AutocompleteInput.module.css */
.autocomplete-wrapper {
position: relative;
}
.autocomplete-options {
position: absolute;
top: 100%;
left: 0;
width: 100%;
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ccc;
background-color: white;
z-index: 1;
}
.autocomplete-options li {
padding: 5px 10px;
cursor: pointer;
}
.autocomplete-options li:hover {
background-color: #f0f0f0;
}
在这个示例中,我们添加了 options 属性,用于指定自动完成的选项列表。当输入框内容变化时,根据输入值过滤选项列表,并显示匹配的选项。用户点击选项时,将选项值设置为输入框的值。
四、下拉菜单组件(Select)
4.1 下拉菜单组件的基本实现
下拉菜单组件用于让用户从多个选项中选择一个。以下是一个简单的 React 下拉菜单组件的实现:
jsx
import React from 'react';
const Select = (props) => {
const {
options, // 下拉菜单的选项列表
value, // 当前选中的值
onChange, // 选项改变时的回调函数
className // 自定义的类名
} = props;
return (
<select
value={value}
onChange={onChange}
className={className}
>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
export default Select;
在这个下拉菜单组件中,我们接收了三个主要属性:options 是一个包含选项信息的数组,每个选项对象包含 value 和 label 属性;value 是当前选中的值;onChange 是选项改变时触发的回调函数;className 用于自定义下拉菜单的样式。
4.2 下拉菜单组件的分组功能
有时候,下拉菜单的选项需要进行分组显示。以下是一个支持分组功能的下拉菜单组件的实现:
jsx
import React from 'react';
const GroupedSelect = (props) => {
const {
groups, // 分组后的选项列表
value,
onChange,
className
} = props;
return (
<select
value={value}
onChange={onChange}
className={className}
>
{groups.map(group => (
<optgroup key={group.label} label={group.label}>
{group.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
))}
</select>
);
};
export default GroupedSelect;
在这个示例中,我们将选项按照分组进行组织,groups 属性是一个包含多个分组对象的数组,每个分组对象包含 label 和 options 属性。使用 <optgroup> 标签将选项进行分组显示。
4.3 下拉菜单组件的搜索功能
当选项较多时,为下拉菜单添加搜索功能可以提高用户体验。以下是一个支持搜索功能的下拉菜单组件的实现:
jsx
import React, { useState } from 'react';
import Input from './Input';
const SearchableSelect = (props) => {
const {
options,
value,
onChange,
className
} = props;
const [searchTerm, setSearchTerm] = useState('');
const [showOptions, setShowOptions] = useState(false);
const filteredOptions = options.filter(option =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const handleOptionSelect = (option) => {
onChange({ target: { value: option.value } });
setShowOptions(false);
setSearchTerm('');
};
return (
<div className="searchable-select-wrapper">
<Input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearchChange}
onClick={() => setShowOptions(true)}
className={className}
/>
{showOptions && (
<ul className="searchable-select-options">
{filteredOptions.map(option => (
<li key={option.value} onClick={() => handleOptionSelect(option)}>
{option.label}
</li>
))}
</ul>
)}
</div>
);
};
export default SearchableSelect;
css
/* SearchableSelect.module.css */
.searchable-select-wrapper {
position: relative;
}
.searchable-select-options {
position: absolute;
top: 100%;
left: 0;
width: 100%;
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ccc;
background-color: white;
z-index: 1;
}
.searchable-select-options li {
padding: 5px 10px;
cursor: pointer;
}
.searchable-select-options li:hover {
background-color: #f0f0f0;
}
在这个示例中,我们添加了一个输入框用于搜索选项。当输入框内容变化时,根据输入值过滤选项列表,并显示匹配的选项。用户点击选项时,将选项值设置为下拉菜单的选中值。
4.4 下拉菜单组件的多选功能
除了单选下拉菜单,有时候我们还需要实现多选功能。以下是一个支持多选功能的下拉菜单组件的实现:
jsx
import React, { useState } from 'react';
const MultiSelect = (props) => {
const {
options,
value = [], // 默认选中的值为空数组
onChange,
className
} = props;
const [selectedOptions, setSelectedOptions] = useState(value);
const handleOptionChange = (option) => {
const isSelected = selectedOptions.includes(option.value);
let newSelectedOptions;
if (isSelected) {
newSelectedOptions = selectedOptions.filter(selected => selected!== option.value);
} else {
newSelectedOptions = [...selectedOptions, option.value];
}
setSelectedOptions(newSelectedOptions);
onChange(newSelectedOptions);
};
return (
<div className="multi-select-wrapper">
{options.map(option => (
<label key={option.value} className="multi-select-option">
<input
type="checkbox"
checked={selectedOptions.includes(option.value)}
onChange={() => handleOptionChange(option)}
/>
{option.label}
</label>
))}
</div>
);
};
export default MultiSelect;
css
/* MultiSelect.module.css */
.multi-select-wrapper {
display: flex;
flex-direction: column;
}
.multi-select-option {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.multi-select-option input {
margin-right: 5px;
}
在这个示例中,我们使用 useState 钩子来管理选中的选项。每个选项对应一个复选框,当复选框状态改变时,更新选中的选项列表,并调用 onChange 回调函数通知父组件。
4.5 下拉菜单组件的动态加载选项
在某些情况下,下拉菜单的选项可能需要动态加载,例如根据用户的输入或其他条件进行查询。以下是一个支持动态加载选项的下拉菜单组件的实现:
jsx
import React, { useState, useEffect } from 'react';
import Input from './Input';
const DynamicSelect = (props) => {
const {
loadOptions, // 加载选项的函数
value,
onChange,
className
} = props;
const [options, setOptions] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [showOptions, setShowOptions] = useState(false);
useEffect(() => {
const fetchOptions = async () => {
const newOptions = await loadOptions(searchTerm);
setOptions(newOptions);
};
fetchOptions();
}, [searchTerm, loadOptions]);
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const handleOptionSelect = (option) => {
onChange(option.value);
setShowOptions(false);
setSearchTerm('');
};
return (
<div className="dynamic-select-wrapper">
<Input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearchChange}
onClick={() => setShowOptions(true)}
className={className}
/>
{showOptions && (
<ul className="dynamic-select-options">
{options.map(option => (
<li key={option.value} onClick={() => handleOptionSelect(option)}>
{option.label}
</li>
))}
</ul>
)}
</div>
);
};
export default DynamicSelect;
在这个示例中,我们添加了一个 loadOptions 属性,它是一个异步函数,用于根据搜索关键词加载选项。使用 useEffect 钩子在搜索关键词变化时调用 loadOptions 函数,更新选项列表。
五、复选框组件(Checkbox)
5.1 复选框组件的基本实现
复选框用于让用户选择一个或多个选项。以下是一个简单的 React 复选框组件的实现:
jsx
import React from 'react';
const Checkbox = (props) => {
const {
label, // 复选框的标签文本
checked, // 复选框的选中状态
onChange, // 复选框状态改变时的回调函数
className // 自定义的类名
} = props;
return (
<label className={className}>
<input
type="checkbox"
checked={checked}
onChange={onChange}
/>
{label}
</label>
);
};
export default Checkbox;
在这个复选框组件中,我们接收了四个属性:label 是复选框旁边的标签文本;checked 表示复选框的选中状态;onChange 是复选框状态改变时触发的回调函数;className 用于自定义复选框的样式。
5.2 复选框组件的组管理
当有多个复选框需要进行组管理时,例如全选、反选等操作。以下是一个支持组管理的复选框组件的实现:
jsx
import React, { useState } from 'react';
import Checkbox from './Checkbox';
const CheckboxGroup = (props) => {
const {
options, // 复选框的选项列表
value = [], // 默认选中的值
onChange // 选项改变时的回调函数
} = props;
const [selectedOptions, setSelectedOptions] = useState(value);
const handleCheckboxChange = (option) => {
const isSelected = selectedOptions.includes(option.value);
let newSelectedOptions;
if (isSelected) {
newSelectedOptions = selectedOptions.filter(selected => selected!== option.value);
} else {
newSelectedOptions = [...selectedOptions, option.value];
}
setSelectedOptions(newSelectedOptions);
onChange(newSelectedOptions);
};
const handleSelectAll = () => {
const allOptions = options.map(option => option.value);
setSelectedOptions(allOptions);
onChange(allOptions);
};
const handleDeselectAll = () => {
setSelectedOptions([]);
onChange([]);
};
return (
<div>
<button onClick={handleSelectAll}>Select All</button>
<button onClick={handleDeselectAll}>Deselect All</button>
{options.map(option => (
<Checkbox
key={option.value}
label={option.label}
checked={selectedOptions.includes(option.value)}
onChange={() => handleCheckboxChange(option)}
/>
))}
</div>
);
};
export default CheckboxGroup;
在这个示例中,我们使用 useState 钩子来管理选中的选项。提供了 “Select All” 和 “Deselect All” 按钮,分别用于全选和反选所有复选框。
5.3 复选框组件的禁用状态
有时候,我们需要将复选框设置为禁用状态,禁止用户操作。以下是一个支持禁用状态的复选框组件的实现:
jsx
import React from 'react';
const DisabledCheckbox = (props) => {
const {
label,
checked,
onChange,
className,
disabled = false // 是否禁用,默认为 false
} = props;
return (
<label className={className}>
<input
type="checkbox"
checked={checked}
onChange={onChange}
disabled={disabled}
/>
{label}
</label>
);
};
export default DisabledCheckbox;
在这个示例中,我们添加了一个 disabled 属性,用于控制复选框是否禁用。当 disabled 为 true 时,复选框将变为不可用状态。
5.4 复选框组件的样式定制
为了满足不同的设计需求,我们可以对复选框的样式进行定制。以下是一个使用 CSS 模块定制复选框样式的示例:
jsx
import React from 'react';
import styles from './Checkbox.module.css';
const StyledCheckbox = (props) => {
const {
label,
checked,
onChange
} = props;
return (
<label className={styles.checkbox}>
<input
type="checkbox"
checked={checked}
onChange={onChange}
/>
<span className={styles.checkmark}></span>
{label}
</label>
);
};
export default StyledCheckbox;
css
/* Checkbox.module.css */
.checkbox {
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
background-color: #eee;
}
.checkbox:hover input ~.checkmark {
background-color: #ccc;
}
.checkbox input:checked ~.checkmark {
background-color: #2196F3;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox input:checked ~.checkmark:after {
display: block;
}
.checkbox.checkmark:after {
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
在这个示例中,我们使用 CSS 模块创建了一个自定义样式的复选框。通过隐藏原生的复选框,使用一个自定义的 span 元素作为复选框的外观,并通过 CSS 伪类和选择器来实现选中状态的样式变化。
5.5 复选框组件的半选状态
在某些场景下,复选框可能需要显示半选状态,例如在多级选择中。以下是一个支持半选状态的复选框组件的实现:
jsx
import React from 'react';
const IndeterminateCheckbox = (props) => {
const {
label,
checked,
onChange,
indeterminate = false // 是否为半选状态,默认为 false
} = props;
const checkboxRef = React.createRef();
React.useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
return (
<label>
<input
type="checkbox"
ref={checkboxRef}
checked={checked}
onChange={onChange}
/>
{label}
</label>
);
};
export default IndeterminateCheckbox;
在这个示例中,我们添加了一个 indeterminate 属性,用于控制复选框是否显示半选状态。使用 React.createRef() 创建一个引用,在 useEffect 钩子中根据 indeterminate 的值设置复选框的 indeterminate 属性。
六、单选框组件(Radio)
6.1 单选框组件的基本实现
单选框用于让用户从多个选项中选择一个。以下是一个简单的 React 单选框组件的实现:
jsx
import React from 'react';
const Radio = (props) => {
const {
label, // 单选框的标签文本
value, // 单选框的值
checked, // 单选框的选中状态
onChange, // 单选框状态改变时的回调函数
className // 自定义的类名
} = props;
return (
<label className={className}>
<input
type="radio"
value={value}
checked={checked}
onChange={onChange}
/>
{label}
</label>
);
};
export default Radio;
在这个单选框组件中,我们接收了五个属性:label 是单选框旁边的标签文本;value 是单选框的值;checked 表示单选框的选中状态;onChange 是单选框状态改变时触发的回调函数;className 用于自定义单选框的样式。
6.2 单选框组件的组管理
与复选框类似,单选框通常也需要进行组管理,确保同一组内只有一个单选框可以被选中。以下是一个支持组管理的单选框组件的实现:
jsx
import React, { useState } from 'react';
import Radio from './Radio';
const RadioGroup = (props) => {
const {
options, // 单选框的选项列表
6.2 单选框组件的组管理
与复选框类似,单选框通常也需要进行组管理,确保同一组内只有一个单选框可以被选中。以下是一个支持组管理的单选框组件的实现:
jsx
import React, { useState } from 'react';
import Radio from './Radio';
const RadioGroup = (props) => {
const {
options, // 单选框的选项列表,每个选项是包含value和label的对象
value, // 当前选中的值
onChange // 选项改变时的回调函数
} = props;
// 使用useState钩子管理当前选中的值,初始值为传入的value
const [selectedValue, setSelectedValue] = useState(value);
// 单选框状态改变时的处理函数
const handleRadioChange = (optionValue) => {
setSelectedValue(optionValue);
// 调用父组件传入的onChange回调,通知选中值变化
onChange(optionValue);
};
return (
<div>
{options.map(option => (
// 为每个选项渲染一个Radio组件
<Radio
key={option.value}
label={option.label}
value={option.value}
// 判断当前选项是否被选中
checked={selectedValue === option.value}
// 绑定状态改变处理函数
onChange={() => handleRadioChange(option.value)}
/>
))}
</div>
);
};
export default RadioGroup;
在上述代码中,RadioGroup 组件通过 useState 管理组内当前选中的单选框值。当某个单选框状态改变时,更新 selectedValue 并触发父组件的 onChange 回调。每个 Radio 组件根据 selectedValue 判断自身是否处于选中状态。
6.3 单选框组件的禁用状态
和其他组件一样,单选框也可能需要设置为禁用状态,禁止用户进行选择操作。实现如下:
jsx
import React from 'react';
const DisabledRadio = (props) => {
const {
label,
value,
checked,
onChange,
className,
disabled = false // 是否禁用,默认值为false
} = props;
return (
<label className={className}>
<input
type="radio"
value={value}
checked={checked}
onChange={onChange}
// 根据disabled属性设置是否禁用
disabled={disabled}
/>
{label}
</label>
);
};
export default DisabledRadio;
在这个 DisabledRadio 组件中,通过 disabled 属性控制单选框的禁用状态。当 disabled 为 true 时,input 元素的 disabled 属性被设置,单选框呈现不可操作状态。
6.4 单选框组件的样式定制
为了符合不同的设计风格,单选框的样式也需要进行定制。这里通过 CSS 模块实现自定义样式:
jsx
import React from 'react';
import styles from './Radio.module.css';
const StyledRadio = (props) => {
const {
label,
value,
checked,
onChange
} = props;
return (
<label className={styles.radio}>
<input
type="radio"
value={value}
checked={checked}
onChange={onChange}
/>
<span className={styles.radioCheckmark}></span>
{label}
</label>
);
};
export default StyledRadio;
css
/* Radio.module.css */
.radio {
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.radio input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.radioCheckmark {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
background-color: #eee;
border-radius: 50%;
}
.radio:hover input ~.radioCheckmark {
background-color: #ccc;
}
.radio input:checked ~.radioCheckmark {
background-color: #2196F3;
}
.radioCheckmark:after {
content: "";
position: absolute;
display: none;
}
.radio input:checked ~.radioCheckmark:after {
display: block;
}
.radio.radioCheckmark:after {
top: 9px;
left: 9px;
width: 7px;
height: 7px;
border-radius: 50%;
background: white;
}
上述代码中,通过隐藏原生单选框,使用自定义的 span 元素作为单选框外观。利用 CSS 伪类和选择器,实现单选框未选中、悬停和选中状态下的不同样式展示。
6.5 单选框组件与表单结合
在实际应用中,单选框通常会作为表单的一部分。以下是一个包含单选框组的表单示例:
jsx
import React, { useState } from 'react';
import RadioGroup from './RadioGroup';
const RadioForm = () => {
const options = [
{ value: 'option1', label: '选项一' },
{ value: 'option2', label: '选项二' },
{ value: 'option3', label: '选项三' }
];
// 管理表单中单选框组的选中值
const [selectedOption, setSelectedOption] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// 表单提交时,打印选中的选项值
console.log('Selected option:', selectedOption);
};
return (
<form onSubmit={handleSubmit}>
<RadioGroup
options={options}
value={selectedOption}
onChange={(value) => setSelectedOption(value)}
/>
<button type="submit">提交</button>
</form>
);
};
export default RadioForm;
在 RadioForm 组件中,通过 useState 管理单选框组的选中值。当用户提交表单时,handleSubmit 函数会被触发,此时可以获取并处理选中的选项值,比如发送到后端服务器。
七、标签组件(Label)
7.1 标签组件的基本实现
标签组件主要用于为其他 UI 元素(如输入框、单选框、复选框等)提供描述性文本。其基本实现如下:
jsx
import React from 'react';
const Label = (props) => {
const {
children, // 标签内的文本内容
htmlFor, // 与关联表单元素的id属性对应
className // 自定义的类名
} = props;
return (
<label
htmlFor={htmlFor} // 设置关联表单元素的id
className={className}
>
{children}
</label>
);
};
export default Label;
在这个 Label 组件中,htmlFor 属性用于关联对应的表单元素,当用户点击标签文本时,会将焦点切换到关联的表单元素上。children 表示标签内显示的文本内容,className 用于自定义样式。
7.2 标签组件的样式定制
为了使标签文本更突出或符合设计要求,需要对标签组件进行样式定制。使用 CSS 模块实现如下:
jsx
import React from 'react';
import styles from './Label.module.css';
const StyledLabel = (props) => {
const {
children,
htmlFor,
className
} = props;
// 合并自定义类名和CSS模块中的类名
const combinedClassName = `${styles.label} ${className}`;
return (
<label
htmlFor={htmlFor}
className={combinedClassName}
>
{children}
</label>
);
};
export default StyledLabel;
css
/* Label.module.css */
.label {
font-weight: bold;
color: #333;
display: block;
margin-bottom: 5px;
}
上述代码通过 CSS 模块定义了 label 的基本样式,如加粗字体、颜色、显示方式和底部边距。在组件中通过合并类名应用这些样式,同时也支持添加自定义类名进一步定制。
7.3 标签组件与表单元素的关联应用
标签组件通常与表单元素配合使用,以下是一个完整的表单示例,展示标签组件如何与输入框、单选框和复选框关联:
jsx
import React, { useState } from 'react';
import Label from './Label';
import Input from './Input';
import RadioGroup from './RadioGroup';
import CheckboxGroup from './CheckboxGroup';
const FormExample = () => {
const [username, setUsername] = useState('');
const radioOptions = [
{ value: 'male', label: '男' },
{ value: 'female', label: '女' }
];
const [gender, setGender] = useState('');
const checkboxOptions = [
{ value: 'option1', label: '爱好一' },
{ value: 'option2', label: '爱好二' },
{ value: 'option3', label: '爱好三' }
];
const [hobbies, setHobbies] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
// 打印表单数据
console.log('Username:', username);
console.log('Gender:', gender);
console.log('Hobbies:', hobbies);
};
return (
<form onSubmit={handleSubmit}>
<Label htmlFor="username" className="form-label">用户名:</Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="form-input"
/>
<Label htmlFor="gender" className="form-label">性别:</Label>
<RadioGroup
options={radioOptions}
value={gender}
onChange={(value) => setGender(value)}
/>
<Label htmlFor="hobbies" className="form-label">爱好:</Label>
<CheckboxGroup
options={checkboxOptions}
value={hobbies}
onChange={(values) => setHobbies(values)}
/>
<button type="submit">提交</button>
</form>
);
};
export default FormExample;
在这个表单示例中,每个 Label 组件通过 htmlFor 属性与对应的表单元素(Input、RadioGroup、CheckboxGroup)关联。用户在填写和选择表单内容后,点击提交按钮,handleSubmit 函数会获取并处理表单数据。
7.4 标签组件的国际化支持
在多语言应用中,标签组件的文本内容需要支持国际化。可以借助 react-i18next 库实现,以下是一个简单示例:
jsx
import React from 'react';
import { useTranslation } from'react-i18next';
const InternationalizedLabel = (props) => {
const {
key, // 用于翻译的键
className
} = props;
const { t } = useTranslation();
return (
<label className={className}>
{t(key)}
</label>
);
};
export default InternationalizedLabel;
jsx
import React from'react';
import InternationalizedLabel from './InternationalizedLabel';
const InternationalizedForm = () => {
return (
<form>
<InternationalizedLabel key="username_label" className="form-label" />
<input type="text" />
<InternationalizedLabel key="password_label" className="form-label" />
<input type="password" />
<button type="submit">
<InternationalizedLabel key="submit_button" />
</button>
</form>
);
};
export default InternationalizedForm;
在上述代码中,InternationalizedLabel 组件通过 react-i18next 库的 useTranslation 钩子获取翻译函数 t。根据传入的 key,t 函数会从翻译资源文件中获取对应的本地化文本,实现标签组件的国际化支持 。
八、表格组件(Table)
8.1 表格组件的基本实现
表格组件用于以行列形式展示数据。一个简单的 React 表格组件实现如下:
jsx
import React from'react';
const Table = (props) => {
const {
headers, // 表头数据数组
data // 表格数据数组,每个元素是包含列数据的对象
} = props;
return (
<table>
<thead>
<tr>
{headers.map(header => (
<th key={header}>{header}</th>
))}
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id}>
{Object.values(row).map(cell => (
<td key={cell}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default Table;
在这个 Table 组件中,headers 数组用于渲染表头,data 数组包含表格的行数据。通过 map 方法分别循环渲染表头和表体内容。
8.2 表格组件的样式定制
为了让表格更美观和易读,需要进行样式定制。使用 CSS 模块实现如下:
jsx
import React from'react';
import styles from './Table.module.css';
const StyledTable = (props) => {
const {
headers,
data
} = props;
return (
<table className={styles.table}>
<thead>
<tr>
{headers.map(header => (
<th key={header} className={styles.tableHeader}>{header}</th>
))}
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id} className={styles.tableRow}>
{Object.values(row).map(cell => (
<td key={cell} className={styles.tableCell}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default StyledTable;
css
/* Table.module.css */
.table {
border-collapse: collapse;
width: 100%;
}
.tableHeader {
background-color: #f2f2f2;
text-align: left;
padding: 8px;
}
.tableRow:nth-child(even) {
background-color: #f9f9f9;
}
.tableCell {
border: 1px solid #ddd;
padding: 8px;
}
上述代码通过 CSS 模块定义了表格的边框样式、表头背景色、奇偶行不同背景色以及单元格样式。在组件中通过类名应用这些样式,提升表格的视觉效果。
8.3 表格组件的排序功能
为了方便用户对表格数据进行排序,需要为表格添加排序功能。实现如下:
jsx
import React, { useState } from'react';
const SortableTable = (props) => {
const {
headers,
data
} = props;
// 记录当前排序的列索引,-1表示未排序
const [sortColumn, setSortColumn] = useState(-1);
// 记录排序方向,1为升序,-1为降序
const [sortDirection, setSortDirection] = useState(1);
// 根据列索引和方向对数据进行排序
const sortedData = data.sort((a, b) => {
if (sortColumn === -1) {
return 0;
}
const keyA = Object.keys(a)[sortColumn];
const keyB = Object.keys(b)[sortColumn];
if (typeof a[keyA] ==='string' && typeof b[keyB] ==='string') {
return sortDirection * a[keyA].localeCompare(b[keyB]);
}
return sortDirection * (a[keyA] - b[keyB]);
});
// 切换排序列和方向的处理函数
const handleSort = (index) => {
if (sortColumn === index) {
setSortDirection(sortDirection * -1);
} else {
setSortColumn(index);
setSortDirection(1);
}
};
return (
<table>
<thead>
<tr>
{headers.map((header, index) => (
<th
key={header}
8.3 表格组件的排序功能
(续上)
jsx
import React, { useState } from'react';
const SortableTable = (props) => {
const {
headers,
data
} = props;
// 记录当前排序的列索引,-1表示未排序
const [sortColumn, setSortColumn] = useState(-1);
// 记录排序方向,1为升序,-1为降序
const [sortDirection, setSortDirection] = useState(1);
// 根据列索引和方向对数据进行排序
const sortedData = data.sort((a, b) => {
if (sortColumn === -1) {
return 0;
}
const keyA = Object.keys(a)[sortColumn];
const keyB = Object.keys(b)[sortColumn];
if (typeof a[keyA] ==='string' && typeof b[keyB] ==='string') {
return sortDirection * a[keyA].localeCompare(b[keyB]);
}
return sortDirection * (a[keyA] - b[keyB]);
});
// 切换排序列和方向的处理函数
const handleSort = (index) => {
if (sortColumn === index) {
setSortDirection(sortDirection * -1);
} else {
setSortColumn(index);
setSortDirection(1);
}
};
return (
<table>
<thead>
<tr>
{headers.map((header, index) => (
<th
key={header}
onClick={() => handleSort(index)} // 绑定点击事件以触发排序
style={{ cursor: 'pointer' }} // 设置鼠标悬停为指针样式
>
{header}
{sortColumn === index && (
<span>
{sortDirection === 1? '▲' : '▼'} {/* 根据排序方向显示箭头 */}
</span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map(row => (
<tr key={row.id}>
{Object.values(row).map(cell => (
<td key={cell}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default SortableTable;
在上述代码中,SortableTable 组件通过 useState 钩子管理当前排序列的索引 sortColumn 和排序方向 sortDirection。当用户点击表头时,handleSort 函数被触发,根据点击的列索引和当前排序状态更新 sortColumn 和 sortDirection。然后,通过 Array.sort 方法对数据进行排序,并在表头显示对应的排序箭头以指示当前排序状态。
8.4 表格组件的分页功能
为了处理大量数据,避免一次性渲染过多内容影响性能,表格通常需要分页功能。实现如下:
jsx
import React, { useState } from'react';
const PaginationTable = (props) => {
const {
headers,
data,
pageSize = 10 // 每页显示的数据条数,默认10条
} = props;
// 当前页码,初始为1
const [currentPage, setCurrentPage] = useState(1);
// 计算总页数
const totalPages = Math.ceil(data.length / pageSize);
// 计算当前页显示的数据
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const currentData = data.slice(startIndex, endIndex);
// 页码改变处理函数
const handlePageChange = (page) => {
setCurrentPage(page);
};
return (
<div>
<table>
<thead>
<tr>
{headers.map(header => (
<th key={header}>{header}</th>
))}
</tr>
</thead>
<tbody>
{currentData.map(row => (
<tr key={row.id}>
{Object.values(row).map(cell => (
<td key={cell}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
<div className="pagination">
<button
disabled={currentPage === 1} // 禁用首页按钮如果当前是第一页
onClick={() => handlePageChange(1)}
>
首页
</button>
<button
disabled={currentPage === 1} // 禁用上一页按钮如果当前是第一页
onClick={() => handlePageChange(currentPage - 1)}
>
上一页
</button>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i + 1}
className={currentPage === i + 1? 'active' : ''} // 给当前页码添加active类
onClick={() => handlePageChange(i + 1)}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === totalPages} // 禁用下一页按钮如果当前是最后一页
onClick={() => handlePageChange(currentPage + 1)}
>
下一页
</button>
<button
disabled={currentPage === totalPages} // 禁用尾页按钮如果当前是最后一页
onClick={() => handlePageChange(totalPages)}
>
尾页
</button>
</div>
</div>
);
};
export default PaginationTable;
css
/* 分页样式 */
.pagination {
display: flex;
justify-content: center;
margin-top: 10px;
}
.pagination button {
margin: 0 5px;
padding: 5px 10px;
cursor: pointer;
}
.pagination button.active {
background-color: #007bff;
color: white;
border: none;
}
在 PaginationTable 组件中,通过 useState 管理当前页码 currentPage。根据 pageSize 计算总页数 totalPages 和当前页显示的数据 currentData。提供了首页、上一页、页码按钮、下一页和尾页的交互功能,用户点击相应按钮时,通过 handlePageChange 函数更新 currentPage 并重新渲染当前页数据。同时,通过 CSS 样式对分页按钮进行美化和状态展示。
8.5 表格组件的筛选功能
表格的筛选功能可以帮助用户快速找到符合特定条件的数据。以下是一个支持简单文本筛选的表格组件实现:
jsx
import React, { useState } from'react';
const FilterableTable = (props) => {
const {
headers,
data
} = props;
// 记录筛选文本,初始为空字符串
const [filterText, setFilterText] = useState('');
// 根据筛选文本过滤后的数据
const filteredData = data.filter(row => {
return Object.values(row).some(cell => {
return String(cell).toLowerCase().includes(filterText.toLowerCase());
});
});
return (
<div>
<input
type="text"
placeholder="输入关键词筛选..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)} // 绑定输入变化事件更新筛选文本
/>
<table>
<thead>
<tr>
{headers.map(header => (
<th key={header}>{header}</th>
))}
</tr>
</thead>
<tbody>
{filteredData.map(row => (
<tr key={row.id}>
{Object.values(row).map(cell => (
<td key={cell}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export default FilterableTable;
在 FilterableTable 组件中,通过 useState 管理筛选文本 filterText。当输入框内容发生变化时,onChange 事件触发更新 filterText。然后,通过 Array.filter 方法对原始数据 data 进行筛选,只保留包含筛选文本的数据行,并渲染过滤后的数据。
8.6 表格组件的单元格编辑功能
有时需要在表格中直接编辑单元格内容,以下是一个支持单元格编辑的表格组件实现:
jsx
import React, { useState } from'react';
const EditableTable = (props) => {
const {
headers,
data
} = props;
// 记录处于编辑状态的单元格坐标 [行索引, 列索引],初始为null
const [editCell, setEditCell] = useState(null);
// 深拷贝数据,用于编辑操作
const [tableData, setTableData] = useState([...data]);
// 进入编辑状态的处理函数
const startEdit = (rowIndex, colIndex) => {
setEditCell([rowIndex, colIndex]);
};
// 保存编辑内容的处理函数
const saveEdit = (rowIndex, colIndex, newValue) => {
const newData = [...tableData];
const key = Object.keys(newData[rowIndex])[colIndex];
newData[rowIndex][key] = newValue;
setTableData(newData);
setEditCell(null);
};
// 取消编辑的处理函数
const cancelEdit = () => {
setEditCell(null);
};
return (
<table>
<thead>
<tr>
{headers.map(header => (
<th key={header}>{header}</th>
))}
</tr>
</thead>
<tbody>
{tableData.map((row, rowIndex) => (
<tr key={row.id}>
{Object.entries(row).map(([key, cell], colIndex) => (
{editCell && editCell[0] === rowIndex && editCell[1] === colIndex? (
<td key={key}>
<input
type="text"
value={cell}
onBlur={() => saveEdit(rowIndex, colIndex, event.target.value)} // 失去焦点时保存编辑
onKeyDown={(e) => {
if (e.key === 'Escape') {
cancelEdit(); // 按下Esc键取消编辑
}
}}
/>
</td>
) : (
<td key={key}>
{cell}
<button
onClick={() => startEdit(rowIndex, colIndex)} // 点击按钮进入编辑状态
style={{ display: editCell && editCell[0] === rowIndex && editCell[1] === colIndex? 'none' : 'inline' }}
>
编辑
</button>
</td>
)}
))}
</tr>
))}
</tbody>
</table>
);
};
export default EditableTable;
在 EditableTable 组件中,通过 useState 管理处于编辑状态的单元格坐标 editCell 和表格数据 tableData。当用户点击单元格的 “编辑” 按钮时,startEdit 函数被触发,设置 editCell 进入编辑状态。在编辑过程中,用户输入内容后,失去焦点时 saveEdit 函数保存编辑内容;按下 Esc 键时,cancelEdit 函数取消编辑操作,恢复单元格显示原始内容。
九、模态框组件(Modal)
9.1 模态框组件的基本实现
模态框常用于在页面上弹出重要信息或提示用户进行确认操作。以下是一个简单的 React 模态框组件实现:
jsx
import React, { useState } from'react';
import './Modal.css';
const Modal = (props) => {
const {
title, // 模态框标题
content, // 模态框内容
isOpen, // 模态框是否显示
onClose // 关闭模态框的回调函数
} = props;
return (
{isOpen && (
<div className="modal-overlay">
<div className="modal-content">
<h2 className="modal-title">{title}</h2>
<div className="modal-body">{content}</div>
<button className="modal-close" onClick={onClose}>关闭</button>
</div>
</div>
)}
);
};
export default Modal;
css
/* Modal.css */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
.modal-title {
margin-top: 0;
}
.modal-close {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
}
在 Modal 组件中,通过判断 isOpen 属性决定是否渲染模态框。当 isOpen 为 true 时,显示一个覆盖整个页面的半透明遮罩层(.modal-overlay),并在中间显示模态框内容(.modal-content),包含标题、主体内容和关闭按钮。关闭按钮点击时触发 onClose 回调函数关闭模态框。
9.2 模态框组件的动画效果
为了提升用户体验,模态框可以添加动画效果,如淡入淡出或滑入滑出。以下是一个添加淡入淡出动画的模态框组件实现:
jsx
import React, { useState } from'react';
import './AnimatedModal.css';
const AnimatedModal = (props) => {
const {
title,
content,
isOpen,
onClose
} = props;
return (
{isOpen && (
<div className="animated-modal-overlay fade-in">
<div className="animated-modal-content">
<h2 className="modal-title">{title}</h2>
<div className="modal-body">{content}</div>
<button className="modal-close" onClick={onClose}>关闭</button>
</div>
</div>
)}
);
};
export default AnimatedModal;
css
/* AnimatedModal.css */
.animated-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.fade-in {
opacity: 1;
}
.animated-modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
transform: translateY(-20px);
opacity: 0;
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition-delay: 0.1s;
}
.fade-in.animated-modal-content {
transform: translateY(0);
opacity: 1;
}
在上述代码中,通过 CSS 过渡(transition)属性为模态框的遮罩层和内容添加淡入淡出及滑入的动画效果。当 isOpen 为 true 时,添加 fade-in 类名,触发相应的动画过渡,使模态框以更平滑的方式显示和隐藏。
9.3 模态框组件的嵌套使用
在一些复杂场景下,可能需要在一个模态框中打开另一个模态框,即模态框的嵌套使用。以下是一个支持嵌套的模态框组件示例:
jsx
import React, { useState } from'react';
import Modal from './Modal';
const NestedModalExample = () => {
const [mainModalOpen, setMainModalOpen] = useState(false);
const [nestedModalOpen, setNestedModalOpen] = useState(false);
return (
<div>
<button onClick={() => setMainModalOpen(true)}>打开主模态框</button>
{mainModalOpen && (
<Modal
title="主模态框"
content="这是主模态框的内容"
isOpen={mainModalOpen}
onClose={()
9.3 模态框组件的嵌套使用
(续上)
jsx
import React, { useState } from'react';
import Modal from './Modal';
const NestedModalExample = () => {
const [mainModalOpen, setMainModalOpen] = useState(false);
const [nestedModalOpen, setNestedModalOpen] = useState(false);
return (
<div>
<button onClick={() => setMainModalOpen(true)}>打开主模态框</button>
{mainModalOpen && (
<Modal
title="主模态框"
content="这是主模态框的内容
<button onClick={() => setNestedModalOpen(true)}>打开嵌套模态框</button>"
isOpen={mainModalOpen}
onClose={() => setMainModalOpen(false)}
/>
)}
{nestedModalOpen && (
<Modal
title="嵌套模态框"
content="这是嵌套模态框的内容"
isOpen={nestedModalOpen}
onClose={() => setNestedModalOpen(false)}
/>
)}
</div>
);
};
export default NestedModalExample;
在这个示例中,通过两个状态变量 mainModalOpen 和 nestedModalOpen 分别控制主模态框和嵌套模态框的显示与隐藏。在主模态框的内容中添加一个按钮,点击该按钮时设置 nestedModalOpen 为 true 以打开嵌套模态框。每个模态框都有独立的关闭回调函数,确保关闭操作互不干扰。
9.4 模态框组件的自定义按钮配置
除了默认的关闭按钮,有时需要在模态框中添加自定义按钮以执行特定操作。以下是支持自定义按钮配置的模态框组件实现:
jsx
import React from'react';
import './Modal.css';
const CustomButtonModal = (props) => {
const {
title,
content,
isOpen,
onClose,
customButtons // 自定义按钮数组,每个元素是包含label和onClick的对象
} = props;
return (
{isOpen && (
<div className="modal-overlay">
<div className="modal-content">
<h2 className="modal-title">{title}</h2>
<div className="modal-body">{content}</div>
<div className="modal-buttons">
{customButtons.map((button, index) => (
<button
key={index}
className="modal-custom-button"
onClick={button.onClick}
>
{button.label}
</button>
))}
<button className="modal-close" onClick={onClose}>关闭</button>
</div>
</div>
</div>
)}
);
};
export default CustomButtonModal;
css
/* 新增自定义按钮样式 */
.modal-buttons {
display: flex;
justify-content: space-around;
margin-top: 10px;
}
.modal-custom-button {
padding: 5px 10px;
cursor: pointer;
border: none;
background-color: #007bff;
color: white;
border-radius: 3px;
}
.modal-custom-button:hover {
background-color: #0056b3;
}
在 CustomButtonModal 组件中,新增 customButtons 属性,它是一个数组,每个元素包含按钮的文本 label 和点击回调函数 onClick。在组件内部,通过 map 方法循环渲染自定义按钮,并将其与默认的关闭按钮一起展示在模态框底部。
9.5 模态框组件的尺寸和位置定制
为了适应不同的使用场景,模态框的尺寸和位置可能需要进行定制。以下是支持尺寸和位置定制的模态框组件实现:
jsx
import React from'react';
import './Modal.css';
const CustomSizePositionModal = (props) => {
const {
title,
content,
isOpen,
onClose,
width = '500px', // 模态框宽度,默认500px
height = '300px', // 模态框高度,默认300px
top = '50%', // 模态框顶部位置,默认垂直居中
left = '50%', // 模态框左侧位置,默认水平居中
transform = 'translate(-50%, -50%)' // 用于定位的transform属性值
} = props;
return (
{isOpen && (
<div className="modal-overlay">
<div
className="modal-content"
style={{
width,
height,
top,
left,
transform
}}
>
<h2 className="modal-title">{title}</h2>
<div className="modal-body">{content}</div>
<button className="modal-close" onClick={onClose}>关闭</button>
</div>
</div>
)}
);
};
export default CustomSizePositionModal;
在 CustomSizePositionModal 组件中,通过新增 width、height、top、left 和 transform 属性,允许用户自定义模态框的宽度、高度、顶部位置、左侧位置以及定位方式。在渲染时,将这些属性通过 style 对象应用到模态框内容元素上,实现尺寸和位置的灵活定制。
十、通知提示组件(Toast)
10.1 通知提示组件的基本实现
通知提示组件用于在页面上短暂显示提示信息,不打断用户的主要操作流程。以下是一个简单的 React 通知提示组件实现:
jsx
import React, { useState } from'react';
import './Toast.css';
const Toast = (props) => {
const {
message, // 提示信息内容
duration = 3000 // 提示信息显示时长,默认3秒
} = props;
const [showToast, setShowToast] = useState(true);
React.useEffect(() => {
const timer = setTimeout(() => {
setShowToast(false);
}, duration);
return () => clearTimeout(timer);
}, [duration]);
return (
{showToast && (
<div className="toast">
<p>{message}</p>
</div>
)}
);
};
export default Toast;
css
/* Toast.css */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1001;
}
在 Toast 组件中,通过 useState 管理提示信息的显示状态 showToast,默认显示。使用 useEffect 钩子在组件挂载时启动一个定时器,定时结束后将 showToast 设置为 false 以隐藏提示信息。提示信息默认显示在页面底部中间位置,通过 CSS 样式设置背景、文本颜色、边框等样式。
10.2 通知提示组件的多种类型
通知提示组件可能需要展示不同类型的提示,如成功提示、警告提示、错误提示等。以下是支持多种类型的通知提示组件实现:
jsx
import React, { useState } from'react';
import './Toast.css';
const TypeToast = (props) => {
const {
message,
type ='info', // 提示类型,默认info
duration = 3000
} = props;
const [showToast, setShowToast] = useState(true);
React.useEffect(() => {
const timer = setTimeout(() => {
setShowToast(false);
}, duration);
return () => clearTimeout(timer);
}, [duration]);
return (
{showToast && (
<div className={`toast toast-${type}`}>
<p>{message}</p>
</div>
)}
);
};
export default TypeToast;
css
/* 新增不同类型的提示样式 */
.toast-info {
background-color: #007bff;
}
.toast-success {
background-color: #28a745;
}
.toast-warning {
background-color: #ffc107;
}
.toast-error {
background-color: #dc3545;
}
在 TypeToast 组件中,新增 type 属性用于指定提示类型。通过在 CSS 中定义不同类型对应的样式类(如 .toast-info、.toast-success 等),在组件渲染时根据 type 值动态添加相应的类名,实现不同类型提示信息的样式区分。
10.3 通知提示组件的队列管理
当短时间内有多个通知提示需要显示时,需要进行队列管理,避免多个提示重叠显示。以下是支持队列管理的通知提示组件实现:
jsx
import React, { useState, useEffect, useRef } from'react';
import './Toast.css';
const ToastQueue = (props) => {
const {
messages // 提示信息数组,每个元素是包含message和type的对象
} = props;
const [toastQueue, setToastQueue] = useState([]);
const timeoutRef = useRef(null);
useEffect(() => {
setToastQueue(messages);
}, [messages]);
useEffect(() => {
const showNextToast = () => {
if (toastQueue.length > 0) {
const { message, type } = toastQueue[0];
// 模拟显示Toast组件
setTimeout(() => {
setToastQueue(prevQueue => prevQueue.slice(1));
}, 3000);
}
};
if (toastQueue.length > 0 && timeoutRef.current === null) {
timeoutRef.current = setTimeout(showNextToast, 0);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, [toastQueue]);
return (
<div>
{toastQueue.map(({ message, type }, index) => (
<div key={index} className={`toast toast-${type}`}>
<p>{message}</p>
</div>
))}
</div>
);
};
export default ToastQueue;
在 ToastQueue 组件中,通过 useState 管理提示信息队列 toastQueue,useEffect 钩子在 messages 数据更新时更新队列。另一个 useEffect 钩子用于处理队列中提示信息的显示逻辑,按照顺序依次显示每个提示信息,每个提示显示 3 秒后从队列中移除,确保提示信息有序显示且不重叠。
10.4 通知提示组件的全局调用
为了方便在应用的各个部分调用通知提示组件,通常会实现全局调用的方式。以下是通过 React Context 实现全局调用通知提示组件的示例:
jsx
import React, { createContext, useState, useEffect } from'react';
import Toast from './Toast';
const ToastContext = createContext();
const ToastProvider = (props) => {
const [toastQueue, setToastQueue] = useState([]);
const showToast = (message, type = 'info', duration = 3000) => {
setToastQueue(prevQueue => [...prevQueue, { message, type, duration }]);
};
useEffect(() => {
const handleQueue = () => {
if (toastQueue.length > 0) {
const { message, type, duration } = toastQueue[0];
setTimeout(() => {
setToastQueue(prevQueue => prevQueue.slice(1));
}, duration);
}
};
if (toastQueue.length > 0) {
handleQueue();
}
return () => {
// 清理可能存在的定时器
};
}, [toastQueue]);
return (
<ToastContext.Provider value={{ showToast }}>
{props.children}
{toastQueue.map(({ message, type }, index) => (
<Toast key={index} message={message} type={type} />
))}
</ToastContext.Provider>
);
};
export { ToastContext, ToastProvider };
使用方式:
jsx
import React, { useContext } from'react';
import { ToastContext } from './ToastProvider';
const App = () => {
const { showToast } = useContext(ToastContext);
const handleClick = () => {
showToast('这是一条提示信息', 'info');
};
return (
<div>
<button onClick={handleClick}>显示提示</button>
</div>
);
};
export default App;
在上述代码中,通过 createContext 创建 ToastContext,在 ToastProvider 组件中管理提示信息队列和 showToast 方法。在应用的根组件中包裹 ToastProvider,其他组件通过 useContext 钩子获取 showToast 方法,实现全局调用通知提示组件的功能。
十一、总结与展望
11.1 总结
通过对 React 基础 UI 组件的源码级分析,我们深入了解了按钮、输入框、下拉菜单、复选框、单选框、标签、表格、模态框和通知提示等组件的实现原理和常见扩展功能。从基础的组件结构到复杂的功能定制,每个组件都体现了 React 组件化开发的思想。
在实现过程中,我们运用了 React 的核心特性,如函数式组件、状态管理(useState)、副作用处理(useEffect)以及组件间通信等。同时,结合 CSS 样式定制和各种 JavaScript 逻辑,实现了丰富多样的 UI 效果和交互功能。这些基础 UI 组件是构建复杂 React 应用的基石,理解它们的内部原理有助于开发者更好地进行组件开发、优化和维护。
11.2 展望
随着前端技术的不断发展,React 基础 UI 组件也将迎来更多的变化和发展:
- 性能优化:未来会有更多针对基础 UI 组件的性能优化方案出现,例如更高效的渲染机制、减少不必要的重渲染等。React 团队也在不断优化核心算法,基础 UI 组件可以从中受益,提升应用整体性能。
- 与新技术融合:随着 Web 技术的更新,如 CSS 新特性(如 CSS Grid、Flexbox 的进一步发展)、Web Components 等,基础 UI 组件可能会与这些新技术更好地融合,提供更强大和灵活的功能。同时,与新兴的前端框架和库(如 React Server Components、SWR、TanStack Query 等)的结合,也将拓展基础 UI 组件的应用场景。
- 低代码 / 无代码开发:低代码和无代码开发平台越来越受欢迎,基础 UI 组件将作为重要的构建块被集成到这些平台中。未来可能会出现更标准化、可配置化的基础 UI 组件,方便开发者通过可视化界面快速搭建应用,降低开发门槛。
- 无障碍性和国际化:对无障碍性和国际化的要求日益增加,基础 UI 组件将更加注重对残障人士的友好支持(如更好的键盘导航、屏幕阅读器兼容性),以及更完善的多语言和本地化支持,以满足全球用户的需求 。