尽管React是全球最受欢迎和使用最广泛的前端框架之一,但许多开发者在重构代码以提高可复用性时仍然感到困难。如果你发现自己在React应用中不断重复相同的代码片段,那你来对地方了。
在本教程中,将向你介绍三个最常见的特征,表明是时候构建一个可重用的 React 组件了。然后我们将继续通过构建一个可重用的布局和两个令人兴奋的 React hooks 来查看一些实际演示。
当你读完后,你就能自己弄清楚什么时候适合创建可重用的 React 组件,以及如何创建。
一、可重用 React 组件的前三个特征
1、重复创建具有相同 CSS 样式的wrappers
知道何时创建可重用组件,我最喜欢的标志是重复使用相同的 CSS 样式。现在,您可能会想,“等一下:为什么不简单地将相同的类名分配给共享相同 CSS 样式的元素呢?你说得完全正确。每次不同组件中的某些元素共享相同的样式时都创建可重用的组件不是一个好主意。事实上,它可能会带来不必要的复杂性。所以你得再问自己一件事:这些通常样式的元素是包装器吗?
请考虑以下登录和注册页面:
// Login.js
import './common.css';
function Login() {
return (
<div className='wrapper'>
<main>
{...}
</main>
<footer className='footer'>
{...}
</footer>
</div>
);
}
// SignUp.js
import './common.css';
function Signup() {
return (
<div className='wrapper'>
<main>
{...}
</main>
<footer className='footer'>
{...}
</footer>
</div>
);
}
相同的样式将应用于每个组件的容器 (<div>
元素) 和页脚。所以在这种情况下,你可以创建两个可重用的组件 — <Wrapper />
和 <Footer />
— 并将它们作为 prop 传递给它们。例如,可以按如下方式重构 login 组件:
// Login.js
import Footer from "./Footer.js";
function Login() {
return (
<Wrapper main={{...}} footer={<Footer />} />
);
}
因此,您不再需要在多个页面中导入common.css
或创建相同的<div>
元素来包装所有内容。
2、重复使用事件侦听器
要将事件侦听器附加到元素上,你可以在 useEffect()
中像这样处理它:
// App.js
import { useEffect } from 'react';
function App() {
const handleKeydown = () => {
alert('key is pressed.');
}
useEffect(() => {
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
}
}, []);
return (...);
}
或者你可以直接在 JSX 中执行此操作,如下所示 button 组件:
// Button.js
function Button() {
return (
<button type="button" onClick={() => { alert('Hi!')}}>
Click me!
</button>
);
};
当你想向 document
或 window
添加事件侦听器时,你必须使用第一种方法。但是,您可能已经意识到,第一种方法需要使用 useEffect(),
addEventListener()
和 removeEventListener()
编写更多代码。因此,在这种情况下,创建自定义 hook 将使您的组件更加简洁。
使用事件侦听器有四种可能的方案:
- 相同的事件侦听器,相同的事件处理程序
- 相同的事件侦听器,不同的事件处理程序
- 不同的事件侦听器,相同的事件处理程序
- 不同的事件侦听器,不同的事件处理程序
在第一种情况下,您可以创建一个 hook,其中同时定义了事件侦听器和事件处理程序。
// useEventListener.js
import { useEffect } from 'react';
export default function useKeydown() {
const handleKeydown = () => {
alert('key is pressed.');
}
useEffect(() => {
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
}
}, []);
};
然后,您可以在任何组件中使用此 hook,如下所示:
// App.js
import useKeydown from './useKeydown.js';
function App() {
useKeydown();
return (...);
};
// useEventListener.js
import { useEffect } from 'react';
export default function useEventListener({ event, handler} ) {
useEffect(() => {
document.addEventListener(event, handler);
return () => {
document.removeEventListener(event, handler);
}
}, []);
};
然后,您可以在任何组件中使用此 Hook,如下所示:
// App.js
import useEventListener from './useEventListener.js';
function App() {
const handleKeydown = () => {
alert('key is pressed.');
}
useEventListener('keydown', handleKeydown);
return (...);
};
3、重复使用相同的 GraphQL 脚本
在使 GraphQL 代码可重用时,您真的不需要寻找迹象。对于复杂的应用程序,用于查询或更改的 GraphQL 脚本很容易占用 30-50 行代码,因为要请求的属性很多。如果您多次使用同一个 GraphQL 脚本,我认为它应该有自己的自定义hook。
import { gql, useQuery } from "@apollo/react-hooks";
const GET_POSTS = gql`
query getPosts {
getPosts {
user {
id
name
...
}
emojis {
id
...
}
...
}
`;
const { data, loading, error } = useQuery(GET_POSTS, {
fetchPolicy: "network-only"
});
与其在每个从后端请求的页面中重复此代码,不如为这个特定的 API 创建一个 React hook:
import { gql, useQuery } from "@apollo/react-hooks";
function useGetPosts() {
const GET_POSTS = gql`{...}`;
const { data, loading, error } = useQuery(GET_POSTS, {
fetchPolicy: "network-only"
});
return [data];
}
const Test = () => {
const [data] = useGetPosts();
return (
<div>{data?.map(post => <h1>{post.text}</h1>)}</div>
);
};
二、构建三种可重用的 React 组件
1、布局组件
React 通常用于构建复杂的 Web 应用程序。这意味着需要在 React 中开发大量页面,我怀疑应用程序的每个页面都会有不同的布局。例如,由 30 个页面组成的 Web 应用程序通常使用少于 5 种不同的布局。因此,构建可在许多不同页面中使用的灵活、可重用的布局至关重要。这将为您节省大量代码,从而节省大量时间。
考虑以下 React 功能组件:
import React from "react";
import style from "./Feed.module.css";
export default function Feed() {
return (
<div className={style.FeedContainer}>
<header className={style.FeedHeader}>Header</header>
<main className={style.FeedMain}>
{
<div className={style.ItemList}>
{itemData.map((item, idx) => (
<div key={idx} className={style.Item}>
{item}
</div>
))}
</div>
}
</main>
<footer className={style.FeedFooter}>Footer</footer>
</div>
);
}
const itemData = [1, 2, 3, 4, 5];
这是一个典型的网页,其中包含 <header>、
<main>
、<footer>。
如果还有 30 个这样的网页,你很容易厌倦重复编写 HTML 标签和一遍又一遍地应用相同的样式。
相反,您可以创建一个接收 <header>、<main>
和<footer>作为props的布局组件。
// Layout.js
import React from "react";
import style from "./Layout.module.css";
import PropTypes from "prop-types";
export default function Layout({ header, main, footer }) {
return (
<div className={style.Container}>
<header className={style.Header}>{header}</header>
<main className={style.Main}>{main}</main>
<footer className={style.Footer}>{footer}</footer>
</div>
);
}
Layout.propTypes = {
main: PropTypes.element.isRequired,
header: PropTypes.element,
footer: PropTypes.element
};
// Feed.js
import React from "react";
import Layout from "./Layout";
import style from "./Feed.module.css";
export default function Feed() {
return (
<Layout
header={<div className={style.FeedHeader}>Header</div>}
main={
<div className={style.ItemList}>
{itemData.map((item, idx) => (
<div key={idx} className={style.Item}>
{item}
</div>
))}
</div>
}
footer={<div className={style.FeedFooter}>Footer</div>}
/>
);
}
const itemData = [1, 2, 3, 4, 5];
创建具有粘性元素布局的专业提示
许多开发人员在想要将页眉粘贴到视区顶部或将页脚粘贴到底部时,倾向于使用 position: fixed
或 position: absolute
。但是,对于布局,您应该尽量避免这种情况。
由于布局的元素将是传递的 props 的父元素,因此您希望保持布局元素的样式尽可能简单——以便传递<header>
、 <main>
或 <footer>
能按预期设置样式。因此,我建议将 position: fixed
和 display: flex
应用于布局的最外层元素,并设置 overflow-y: scroll
到该<main>
元素。
/* Layout.module.css */
.Container {
/* Flexbox */
display: flex;
flex-direction: column;
/* Width & Height */
width: 100%;
height: 100%;
/* Misc */
overflow: hidden;
position: fixed;
}
.Main {
/* Width & Height */
width: 100%;
height: 100%;
/* Misc */
overflow-y: scroll;
}
现在,让我们将一些样式应用于你的 Feed 页面:
/* Feed.module.css */
.FeedHeader {
/* Width & Height */
height: 70px;
/* Color & Border */
background-color: teal;
color: beige;
}
.FeedFooter {
/* Width & Height */
height: 70px;
/* Color & Border */
background-color: beige;
color: teal;
}
.ItemList {
/* Flexbox */
display: flex;
flex-direction: column;
}
.Item {
/* Width & Height */
height: 300px;
/* Misc */
color: teal;
}
.FeedHeader,
.FeedFooter,
.Item {
/* Flexbox */
display: flex;
justify-content: center;
align-items: center;
/* Color & Border */
border: 1px solid teal;
/* Misc */
font-size: 35px;
}
2、事件侦听器
通常,同一个事件侦听器在整个 Web 应用程序中被多次使用。在这种情况下,创建自定义 React 钩子是个好主意。让我们通过开发一个 useScrollSaver
钩子来学习如何做到这一点,它保存用户设备在页面上的滚动位置——这样用户就不需要从顶部再次滚动。这个钩子对于列出大量元素(例如帖子和评论)的网页很有用。
让我们分解以下代码:
export default function useScrollSaver(scrollableDiv, pageUrl) {
/* Save the scroll position */
const handleScroll = () => {
sessionStorage.setItem(
`${pageUrl}-scrollPosition`,
scrollableDiv.current.scrollTop.toString()
);
};
useEffect(() => {
if (scrollableDiv.current) {
const scrollableBody = scrollableDiv.current;
scrollableBody.addEventListener("scroll", handleScroll);
return function cleanup() {
scrollableBody.removeEventListener("scroll", handleScroll);
};
}
}, [scrollableDiv, pageUrl]);
/* Restore the saved scroll position */
useEffect(() => {
if (
scrollableDiv.current &&
sessionStorage.getItem(`${pageUrl}-scrollPosition`)
) {
const prevScrollPos = Number(
sessionStorage.getItem(`${pageUrl}-scrollPosition`)
);
scrollableDiv.current.scrollTop = prevScrollPos;
}
}, [scrollableDiv, pageUrl]);
}
你可以看到 useScrollSaver
钩子需要接收两个参数:scrollableDiv
,它必须是一个可滚动的容器,就像上面布局中的<main>
,以及 pageUrl
,它将用作页面的标识符,以便你可以存储多个页面的滚动位置。
第 1 步:保存滚动位置
首先,您需要将 “scroll” 事件侦听器绑定到可滚动容器:
const scrollableBody = scrollableDiv.current;
scrollableBody.addEventListener("scroll", handleScroll);
return function cleanup() {
scrollableBody.removeEventListener("scroll", handleScroll);
};
现在,每次用户滚动 scrollableDiv
时,都会运行一个名为 handleScroll
的函数。在这个函数中,你应该使用 localStorage
或 sessionStorage
来保存滚动位置。区别在于 localStorage
中的数据不会过期,而 sessionStorage
中的数据会在页面会话结束时被清除。您可以使用 setItem(id: string, value: string)
将数据保存在任一存储中:
const handleScroll = () => {
sessionStorage.setItem(
`${pageUrl}-scrollPosition`,
scrolledDiv.current.scrollTop.toString()
);
};
第 2 步:恢复滚动位置
当用户返回网页时,应将用户定向到它或它之前的滚动位置 — 如果有的话。这个位置数据目前保存在 sessionStorage
中,你需要把它拿出来使用。您可以使用 getItem(id: string)
从存储中获取数据。然后,您只需将可滚动容器的 scroll-top
设置为此获取的值:
const prevScrollPos = Number(
sessionStorage.getItem(`${pageUrl}scrollPosition`)
);
scrollableDiv.current.scrollTop = prevScrollPos;
第 3 步:在任何网页中使用 useScrollSaver
钩子
现在你已经完成了自定义钩子的创建,你可以在任何你想要的网页中使用这个钩子,只要你把两个必需的项目传递给钩子:scrollableDiv
和 pageUrl
。让我们回到 Layout.js
并在其中使用您的钩子。这将允许使用此布局的任何网页享受您的滚动保护程序:
// Layout.js
import React, { useRef } from "react";
import style from "./Layout.module.css";
import PropTypes from "prop-types";
import useScrollSaver from "./useScrollSaver";
export default function Layout({ header, main, footer }) {
const scrollableDiv = useRef(null);
useScrollSaver(scrollableDiv, window.location.pathname);
return (
<div className={style.Container}>
<header className={style.Header}>{header}</header>
<main ref={scrollableDiv} className={style.Main}>
{main}
</main>
<footer className={style.Footer}>{footer}</footer>
</div>
);
}
3、查询/更改(特定于 GraphQL)
如果您像我一样喜欢将 GraphQL 与 React 一起使用,您可以通过为 GraphQL 查询或更改创建 React 钩子来进一步减少您的代码库。
请考虑以下运行 GraphQL 查询 getPosts()
的示例:
import { gql, useQuery } from "@apollo/react-hooks";
const GET_POSTS = gql`
query getPosts {
getPosts {
user {
id
name
...
}
emojis {
id
...
}
...
}
`;
const { data, loading, error } = useQuery(GET_POSTS, {
fetchPolicy: "network-only"
});
如果要从后端请求的属性越来越多,您的 GraphQL 脚本将占用越来越多的空间。因此,无需在每次需要运行查询 getPosts()
时都重复 GraphQL 脚本和 useQuery
,而是可以创建以下 React 钩子:
// useGetPosts.js
import { gql, useQuery } from "@apollo/react-hooks";
export default function useGetPosts() {
const GET_POSTS = gql`
query getPosts {
getPosts {
user {
id
name
...
}
emojis {
id
...
}
...
}
`;
const { data, loading, error } = useQuery(GET_POSTS, {
fetchPolicy: "network-only"
});
return [data, loading, error];
}
然后,您可以按如下方式使用 useGetPosts()
钩子:
// Feed.js
import React from "react";
import Layout from "./Layout";
import style from "./Feed.module.css";
import useGetPosts from "./useGetPosts.js";
export default function Feed() {
const [data, loading, error] = useGetPosts();
return (
<Layout
header={<div className={style.FeedHeader}>Header</div>}
main={
<div className={style.ItemList}>
{data?.getPosts.map((item, idx) => (
<div key={idx} className={style.Item}>
{item}
</div>
))}
</div>
}
footer={<div className={style.FeedFooter}>Footer</div>}
/>
);
}
三、关于创建可重用 React 组件的常见问题解答
1、React.js 中的可重用组件是什么?
React.js 中的可重用组件是封装用户界面 (UI) 功能的特定部分的基本代码块,从而能够以模块化和高效的方式构建应用程序。这些组件旨在在整个应用程序甚至不同项目中使用,提供一定程度的代码可重用性,从而显著简化开发。它们通过封装其逻辑和渲染来促进明确的关注点分离,确保其内部实现细节对应用程序的其余部分保持隐藏。这不仅增强了代码的组织和可维护性,还使开发人员能够通过跨团队和项目共享和重用组件来更加协作。
可重用组件的主要优点之一是它们能够保持用户界面的一致性。通过在应用程序的不同部分使用相同的组件,您可以确保统一的外观和感觉,遵守设计准则并创建更精致的用户体验。这些组件通过 props 进行参数化,允许自定义和适应各种用例。此参数化功能在一致性和灵活性之间提供了平衡,使开发人员能够根据每个应用程序功能或页面的特定要求微调组件行为和外观。
可重用性不仅可以节省时间和精力,还可以简化测试过程。隔离和封装的组件更易于测试,您可以创建单元测试来验证其正确性和功能。通过将可重用组件整合到 React 应用程序中,您可以建立可维护、模块化且一致的代码库,最终提高开发效率并提高用户界面的整体质量。
2、React 中可重用组件的示例是什么?
React 中可重用组件的一个常见示例是 “Button” 组件。按钮是用户界面的基本部分,在整个应用程序中用于各种交互。在 React 中创建可重用的 Button 组件可以让你保持一致的外观和行为,同时减少代码重复。下面是一个可重用的 Button 组件的简单示例:
import React from 'react';
const Button = ({ label, onClick, style }) => {
return (
<button
style={style}
onClick={onClick}
>
{label}
</button>
);
};
export default Button;
在此示例中,Button 组件被封装成一个功能组件,它接收三个关键属性:“label”用于按钮上显示的文本,“onClick”用于单击事件处理程序,以及一个可选的“style”属性用于自定义按钮的外观。此组件封装了按钮的 HTML 结构和行为,使其在整个应用程序中易于重用,而无需复制代码。
此 Button 组件的使用展示了其可重用性和灵活性。在父组件(在本例中为 “App”)中,您可以传入特定的标签文本、单击事件处理函数和样式首选项,以创建具有不同用途和外观的不同按钮。通过使用此类可重用组件,您可以保持统一的用户界面样式并提高代码的可维护性。这些组件可以扩展到按钮之外,以包含更复杂的 UI 元素,例如输入字段、卡片或导航栏,从而形成模块化且可维护的 React 应用程序。可重用组件不仅可以加快开发速度,还可以带来更一致、用户友好的界面,该界面符合设计准则和最佳实践。
3、如何在 React 中创建可重用的组件?
在 React.js 中创建可重用组件是构建模块化和可维护应用程序的基本做法。该过程从一个明确的计划开始,您可以在其中确定要封装在组件中的特定功能或用户界面 (UI) 元素。定义组件的用途后,您可以在 React 项目中创建一个新的 JavaScript/JSX 文件。最好按照 React 的命名约定,以大写字母开头命名文件。
在这个新的组件文件中,您可以定义组件的行为和渲染逻辑。组件可以是功能性的,也可以是基于类的,具体取决于您的需要。在参数化组件时,您接受 props 作为输入,这允许您自定义其外观和行为。使用 PropTypes 或 TypeScript 定义 prop 类型可确保类型的安全性和清晰度,从而更容易理解应该如何使用组件。构建组件后,即可在应用程序的其他部分使用。您可以导入和合并它,传入特定的 props 来为每个用例配置其行为。
4、React 中可重用组件有什么好处?
React 中的可重用组件提供了许多好处,可以显着增强开发过程和应用程序的质量。它们通过将复杂的用户界面分解为更小的、独立的构建块来促进模块化,使代码库更有条理和可维护。这些组件专注于特定功能或 UI 元素,允许开发人员独立管理和更新它们,从而降低意外副作用的风险并简化开发过程。
可重用组件的主要优点是它们的可重用性,从而节省时间和精力。您可以在应用程序的不同部分或不同项目中使用相同的组件,而无需重写类似的代码。这不仅加快了开发速度,还确保了一致的用户界面。一致性是另一个关键优势。通过对特定 UI 元素使用相同的组件,您可以保持统一的外观和行为,遵守设计准则并改善用户体验。
此外,可重用组件是高度可定制的。它们接受 props,这使开发人员能够微调其行为和外观以适应不同的用例。这种参数化增强了组件的灵活性和多功能性。当出现问题时,这些隔离的组件更容易测试和调试,从而可以有效地解决问题。此外,通过跨项目共享和重用组件,团队可以更有效地协作,保持统一的 UI 样式,并确保代码一致性,这对于大规模和长期开发工作至关重要。总之,React 中的可重用组件简化了开发,鼓励代码可重用性,保持 UI 一致性,并提供可定制性,最终导致更高效、更高质量的应用程序。
5、在 React 中,什么是好的可重用组件?
React 中设计良好的可重用组件表现出几个关键特征。首先,可重用性是核心属性。创建这些组件时,应确保在应用程序的不同部分甚至单独的项目中使用。为了实现这一点,它们必须高度参数化,允许开发人员通过 prop 自定义它们的外观和行为。这种多功能性是使组件真正可重用的一个基本方面。
封装同样重要。可重用组件应封装其内部逻辑和渲染,从而将实现细节与应用程序的其余部分隔离开来。这种关注点分离可以产生更简洁、更模块化的代码,并简化集成。
模块化是另一个关键属性。好的可重用组件应该具有单一的目的,专注于特定的功能或特定的 UI 元素。这种模块化设计增强了代码的可维护性和调试效率。此外,易于测试至关重要。隔离的组件更易于测试,从而促进创建健壮且可靠的代码。
最后,这些组件应该维护一个清晰的 API,指定它们接受的 props 及其预期用途。这种清晰度有助于其他开发人员了解如何有效地使用组件。当同一组件用于特定 UI 元素时,它们通过促进一致的用户界面,确保应用程序具有凝聚力和用户友好性。从本质上讲,一个好的可重用组件是可重新配置的、封装的、模块化的、易于测试的,并有助于代码的可维护性和 UI 的一致性。
文章翻译自:A Practical Guide to Creating Reusable React Components — SitePoint