- Published on
如何重构 React组件?
- Authors
- Name
- 青雲
一、引言
在React应用开发过程中,随着业务逻辑的增加和功能的扩展,组件可能会变得越来越复杂。初始的组件代码虽然能够实现功能,但在性能、可维护性等方面可能存在诸多问题。本文将以一个复杂的React组件为例,详细阐述如何通过一系列优化措施对其进行重构,使其变得高效且易于维护。
二、初始复杂组件的问题
(一)初始组件代码示例
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
interface Item {
id: number;
name: string;
}
const ComplexComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
useEffect(() => {
fetch('https://api.example.com/items')
.then(response => response.json())
.then(data => {
setItems(data);
setLoading(false);
});
}, []);
const handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
};
const filteredItems = items.filter(item => item.name.includes(filter));
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Name"
/>
<input
type="text"
name="description"
value={formData.description}
onChange={handleInputChange}
placeholder="Description"
/>
<button type="submit">Submit</button>
</form>
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default ComplexComponent;
- 性能问题
- 在数据过滤部分,每次组件重新渲染时都会重新计算
filteredItems
,即使items
和filter
没有改变。这可能导致不必要的计算开销,特别是当数据量较大时。 - 表单输入的处理函数没有进行优化,可能会导致在不必要的时候触发组件重新渲染。
- 在数据过滤部分,每次组件重新渲染时都会重新计算
- 可维护性问题
- 组件代码过于冗长,将数据获取、过滤、表单处理和UI展示等多种功能混合在一起,使得代码难以阅读和理解。如果需要对某个功能进行修改或扩展,很容易影响到其他部分的代码。
三、重构步骤及优化措施
useMemo
缓存过滤结果
(一)使用- 代码示例
import React, { useState, useEffect, useMemo, ChangeEvent, FormEvent } from 'react';
interface Item {
id: number;
name: string;
}
const ComplexComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
useEffect(() => {
fetch('https://api.example.com/items')
.then(response => response.json())
.then(data => {
setItems(data);
setLoading(false);
});
}, []);
const handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
};
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Name"
/>
<input
type="text"
name="description"
value={formData.description}
onChange={handleInputChange}
placeholder="Description"
/>
<button type="submit">Submit</button>
</form>
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default ComplexComponent;
useMemo
是一个React Hook,用于缓存计算结果。在重构后的ComplexComponent
中,通过useMemo
对filteredItems
进行缓存。这样,只有当items
或者filter
发生变化时,才会重新计算过滤结果,避免了不必要的计算,提高了组件的性能。
(二)提取子组件以提高可读性和性能
ItemList
组件
1. - 代码示例
import React from 'react';
interface Item {
id: number;
name: string;
}
interface ItemListProps {
items: Item[];
}
const ItemList: React.FC<ItemListProps> = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
export default React.memo(ItemList);
- 从原始组件中提取出
ItemList
组件,专门负责展示列表项。这个组件接收一个items
数组作为props,然后将每个item
渲染成一个<li>
元素。通过使用React.memo
对ItemList
进行包装,可以避免在其props没有变化时的不必要重新渲染。
FilteredItems
组件
2. - 代码示例
import React, { useMemo } from 'react';
import ItemList from './ItemList';
interface Item {
id: number;
name: string;
}
interface FilteredItemsProps {
items: Item[];
filter: string;
}
const FilteredItems: React.FC<FilteredItemsProps> = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return <ItemList items={filteredItems} />;
};
export default FilteredItems;
- 该组件负责根据
filter
对items
进行过滤,并将过滤后的结果传递给ItemList
组件。同样使用useMemo
来缓存过滤结果,提高性能。
Form
组件
3. - 代码示例
import React, { ChangeEvent, FormEvent } from 'react';
interface FormProps {
formData: { name: string; description: string };
handleInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (event: FormEvent) => void;
}
const Form: React.FC<FormProps> = ({ formData, handleInputChange, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Name"
/>
<input
type="text"
name="description"
value={formData.description}
onChange={handleInputChange}
placeholder="Description"
/>
<button type="submit">Submit</button>
</form>
);
export default React.memo(Form);
- 将表单相关的逻辑和UI提取到
Form
组件中。它接收formData
、handleInputChange
和handleSubmit
作为props,使得表单部分的代码更加独立和易于维护。使用React.memo
包装Form
组件,防止在不必要的时候重新渲染。 - 重构后的
ComplexComponent
整合子组件的代码示例
import React, { useState, useEffect, useMemo, useCallback, ChangeEvent, FormEvent } from 'react';
import Form from './Form';
import FilteredItems from './FilteredItems';
interface Item {
id: number;
name: string;
}
const ComplexComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
useEffect(() => {
fetch('https://api.example.com/items')
.then(response => response.json())
.then(data => {
setItems(data);
setLoading(false);
});
}, []);
const handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
};
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
}, []);
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
}, [formData]);
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<div>
<Form formData={formData} handleInputChange={handleInputChange} handleSubmit={handleSubmit} />
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
<FilteredItems items={items} filter={filter} />
</div>
);
};
export default ComplexComponent;
通过提取这些子组件,原始ComplexComponent
的代码结构变得更加清晰,每个子组件都有明确的职责,提高了代码的可读性和可维护性。
(三)减少不必要的重渲染
useCallback
包装函数示例- 在
ComplexComponent
中,对于handleFilterChange
函数,使用useCallback
进行包装。
- 在
import React, { useState, useEffect, useMemo, useCallback, ChangeEvent, FormEvent } from 'react';
import Form from './Form';
import FilteredItems from './FilteredItems';
interface Item {
id: number;
name: string;
}
const ComplexComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
useEffect(() => {
fetch('https://api.example.com/items')
.then(response => response.json())
.then(data => {
setItems(data);
setLoading(false);
});
}, []);
const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, []);
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({...prevData, [name]: value }));
}, []);
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
}, [formData]);
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<div>
<Form formData={formData} handleInputChange={handleInputChange} handleSubmit={handleSubmit} />
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
<FilteredItems items={items} filter={filter} />
</div>
);
};
export default ComplexComponent;
- 这样可以确保在函数依赖没有变化时,函数的引用不会发生变化,从而避免因为函数引用变化导致的不必要的子组件重新渲染。类似地,
handleInputChange
和handleSubmit
函数也使用useCallback
进行了优化,提高了组件的性能。
React.lazy
和Suspense
进行懒加载子组件
(四)使用import React, { useState, useEffect, useCallback, Suspense, lazy, ChangeEvent, FormEvent, useMemo } from 'react';
const Form = lazy(() => import('./Form'));
const FilteredItems = lazy(() => import('./FilteredItems'));
interface Item {
id: number;
name: string;
}
const ComplexComponent: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
useEffect(() => {
const fetchItems = async () => {
const response = await fetch('https://api.example.com/items');
const data = await response.json();
setItems(data);
setLoading(false);
};
fetchItems();
}, []);
const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, []);
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
}, []);
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
}, [formData]);
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<div>
<Suspense fallback={<div>Loading form...</div>}>
<Form formData={formData} handleInputChange={handleInputChange} handleSubmit={handleSubmit} />
</Suspense>
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
<Suspense fallback={<div>Loading items...</div>}>
<FilteredItems items={filteredItems} filter={filter} />
</Suspense>
</div>
);
};
export default ComplexComponent;
- 懒加载原理
- 在
ComplexComponent
中,使用React.lazy
和Suspense
实现了Form
和FilteredItems
子组件的懒加载。React.lazy
允许我们动态地加载组件,只有在组件真正需要被渲染时才会去加载其对应的代码块。Suspense
则用于在组件加载过程中显示一个备用的UI(fallback),例如显示一个加载提示。这样做可以提高应用的初始加载速度,特别是当组件较大或者应用包含多个页面时,懒加载可以有效地减少初始加载的资源量。
- 在
(五)优化数据获取并控制请求取消
在React应用开发中,优化数据获取以及对请求进行有效的控制(如在必要时取消请求)是提升性能和用户体验的重要环节。这里我们使用react - query
和AbortController
来达成这一目标。
useFetchItems
自定义Hook代码示例
// useFetchItems.ts
import { useQuery } from 'react - query';
const useFetchItems = (url: string) => {
const fetchItems = async ({ signal }: { signal?: AbortSignal }) => {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
const { data, error, isLoading } = useQuery(['items', url], fetchItems, {
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 数据的陈旧时间,5分钟
cacheTime: 10 * 60 * 1000 // 缓存时间,10分钟
});
return { items: data || [], loading: isLoading, error };
};
export default useFetchItems;
react - query
的作用与优势- 数据获取优化方面
react - query
为数据获取提供了强大的功能。在useFetchItems
这个自定义Hook中,我们通过useQuery
函数来获取数据。其中refetchOnWindowFocus
被设置为false
,这是一个非常实用的优化设置。在默认情况下,当浏览器窗口重新获得焦点时,许多数据获取库可能会自动重新获取数据,这在某些场景下可能是不必要的,并且可能会消耗额外的网络资源和影响性能。通过将refetchOnWindowFocus
设为false
,我们避免了这种不必要的重新获取数据操作。- 另外,
staleTime
和cacheTime
的设置对于数据的管理非常关键。staleTime
规定了数据在多长时间内被视为“新鲜”,在这个时间范围内,如果有新的获取数据操作(例如组件重新渲染触发数据获取),react - query
会优先使用缓存数据而不是重新发起网络请求。这里设置为5 * 60 * 1000
毫秒(即5分钟),意味着在5分钟内,相同的数据请求将使用缓存数据。cacheTime
则表示数据在缓存中保留的时长,这里设置为10 * 60 * 1000
毫秒(即10分钟),在这个时间段内,缓存数据将被保留以便快速响应后续可能的相同数据请求。
- 对整体性能的提升
- 这种数据获取的优化方式减少了不必要的网络请求,提高了应用的响应速度。当数据量较大或者网络条件不佳时,这种优化效果更加明显。例如,在一个显示用户信息列表的组件中,如果用户频繁切换页面或者在页面内进行操作,通过合理设置
staleTime
和cacheTime
,可以避免频繁地重新获取用户数据,从而提升了用户体验。
- 这种数据获取的优化方式减少了不必要的网络请求,提高了应用的响应速度。当数据量较大或者网络条件不佳时,这种优化效果更加明显。例如,在一个显示用户信息列表的组件中,如果用户频繁切换页面或者在页面内进行操作,通过合理设置
- 数据获取优化方面
AbortController
在请求取消中的作用- 在数据获取过程中,有时候我们需要在组件卸载或者不再需要获取数据时取消正在进行的网络请求。这时候
AbortController
就发挥了重要作用。在fetchItems
函数中,我们可以接收AbortSignal
作为参数传递给fetch
操作。当组件的状态发生变化(例如用户导航到其他页面,当前组件不再需要显示相关数据)时,我们可以触发AbortController
的abort
方法来取消正在进行的fetch
请求。 - 这样做的好处是避免了资源浪费,特别是在处理长时网络请求或者大量数据请求时。如果不取消这些不必要的请求,可能会导致网络带宽的浪费,同时可能会在请求完成后更新已经不需要更新的组件状态,从而引发潜在的错误。例如,在一个包含多个数据请求的复杂页面中,如果用户快速在不同的视图之间切换,使用
AbortController
可以确保只有与当前视图相关的数据请求被执行,提高了应用的性能和稳定性。
- 在数据获取过程中,有时候我们需要在组件卸载或者不再需要获取数据时取消正在进行的网络请求。这时候
(六)添加错误边界(Error Boundaries)
ErrorBoundary
组件代码示例
1. import React from 'react';
interface ErrorBoundaryProps {
children: React.ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
- 集成到
ComplexComponent
的代码示例
import React, { useState, useCallback, Suspense, lazy, ChangeEvent, FormEvent } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import useFetchItems from './useFetchItems';
import ErrorBoundary from './ErrorBoundary';
const Form = lazy(() => import('./Form'));
const FilteredItems = lazy(() => import('./FilteredItems'));
const queryClient = new QueryClient();
const ComplexComponent: React.FC = () => {
const { items, loading, error } = useFetchItems('https://api.example.com/items');
const [filter, setFilter] = useState("");
const [formData, setFormData] = useState({ name: "", description: "" });
const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, []);
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prevData => ({ ...prevData, [name]: value }));
}, []);
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
console.log("Form submitted:", formData);
setFormData({ name: "", description: "" });
}, [formData]);
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<div>
<Suspense fallback={<div>Loading form...</div>}>
<Form formData={formData} handleInputChange={handleInputChange} handleSubmit={handleSubmit} />
</Suspense>
<input type="text" value={filter} onChange={handleFilterChange} placeholder="Filter items" />
{loading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
<Suspense fallback={<div>Loading items...</div>}>
<FilteredItems items={items} filter={filter} />
</Suspense>
</div>
</ErrorBoundary>
</QueryClientProvider>
);
};
export default ComplexComponent;
- 在
ComplexComponent
中,将整个组件包裹在ErrorBoundary
组件内,这样就可以有效地捕获Form
、FilteredItems
等子组件可能出现的错误。当子组件发生错误时,ErrorBoundary
会将自身的hasError
状态设置为true
,然后渲染一个错误提示(如<h1>Something went wrong.</h1>
),而不是让整个应用崩溃,从而提高了应用的稳定性。
四、总结
通过以上一系列的优化措施,我们对初始的复杂ComplexComponent
进行了全面的重构。从最初的性能问题和可维护性较差的状态,逐步构建成为一个高效、可维护的组件。使用useMemo
缓存计算结果、提取子组件、使用useCallback
减少重渲染、懒加载子组件、优化数据获取以及添加错误边界等技术手段,不仅提高了组件的性能,减少了不必要的资源消耗,还大大提高了代码的可读性和可维护性,使得组件在面对复杂业务逻辑和大规模数据时能够更加稳定和高效地运行。这些优化措施是构建高质量React应用的重要实践经验,可以应用于各种规模和复杂度的React项目中。