Published on

如何重构 React组件?

Authors
  • avatar
    Name
    青雲
    Twitter

一、引言

在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;
  1. 性能问题
    • 在数据过滤部分,每次组件重新渲染时都会重新计算filteredItems,即使itemsfilter没有改变。这可能导致不必要的计算开销,特别是当数据量较大时。
    • 表单输入的处理函数没有进行优化,可能会导致在不必要的时候触发组件重新渲染。
  2. 可维护性问题
    • 组件代码过于冗长,将数据获取、过滤、表单处理和UI展示等多种功能混合在一起,使得代码难以阅读和理解。如果需要对某个功能进行修改或扩展,很容易影响到其他部分的代码。

三、重构步骤及优化措施

(一)使用useMemo缓存过滤结果

  1. 代码示例
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中,通过useMemofilteredItems进行缓存。这样,只有当items或者filter发生变化时,才会重新计算过滤结果,避免了不必要的计算,提高了组件的性能。

(二)提取子组件以提高可读性和性能

1. ItemList组件

  • 代码示例
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.memoItemList进行包装,可以避免在其props没有变化时的不必要重新渲染。

2. FilteredItems组件

  • 代码示例
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;
  • 该组件负责根据filteritems进行过滤,并将过滤后的结果传递给ItemList组件。同样使用useMemo来缓存过滤结果,提高性能。

3. Form组件

  • 代码示例
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组件中。它接收formDatahandleInputChangehandleSubmit作为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的代码结构变得更加清晰,每个子组件都有明确的职责,提高了代码的可读性和可维护性。

(三)减少不必要的重渲染

  1. 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;
  • 这样可以确保在函数依赖没有变化时,函数的引用不会发生变化,从而避免因为函数引用变化导致的不必要的子组件重新渲染。类似地,handleInputChangehandleSubmit函数也使用useCallback进行了优化,提高了组件的性能。

(四)使用React.lazySuspense进行懒加载子组件

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;
  1. 懒加载原理
    • ComplexComponent中,使用React.lazySuspense实现了FormFilteredItems子组件的懒加载。React.lazy允许我们动态地加载组件,只有在组件真正需要被渲染时才会去加载其对应的代码块。Suspense则用于在组件加载过程中显示一个备用的UI(fallback),例如显示一个加载提示。这样做可以提高应用的初始加载速度,特别是当组件较大或者应用包含多个页面时,懒加载可以有效地减少初始加载的资源量。

(五)优化数据获取并控制请求取消

在React应用开发中,优化数据获取以及对请求进行有效的控制(如在必要时取消请求)是提升性能和用户体验的重要环节。这里我们使用react - queryAbortController来达成这一目标。

  1. 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;
  1. react - query的作用与优势
    • 数据获取优化方面
      • react - query为数据获取提供了强大的功能。在useFetchItems这个自定义Hook中,我们通过useQuery函数来获取数据。其中refetchOnWindowFocus被设置为false,这是一个非常实用的优化设置。在默认情况下,当浏览器窗口重新获得焦点时,许多数据获取库可能会自动重新获取数据,这在某些场景下可能是不必要的,并且可能会消耗额外的网络资源和影响性能。通过将refetchOnWindowFocus设为false,我们避免了这种不必要的重新获取数据操作。
      • 另外,staleTimecacheTime的设置对于数据的管理非常关键。staleTime规定了数据在多长时间内被视为“新鲜”,在这个时间范围内,如果有新的获取数据操作(例如组件重新渲染触发数据获取),react - query会优先使用缓存数据而不是重新发起网络请求。这里设置为5 * 60 * 1000毫秒(即5分钟),意味着在5分钟内,相同的数据请求将使用缓存数据。cacheTime则表示数据在缓存中保留的时长,这里设置为10 * 60 * 1000毫秒(即10分钟),在这个时间段内,缓存数据将被保留以便快速响应后续可能的相同数据请求。
    • 对整体性能的提升
      • 这种数据获取的优化方式减少了不必要的网络请求,提高了应用的响应速度。当数据量较大或者网络条件不佳时,这种优化效果更加明显。例如,在一个显示用户信息列表的组件中,如果用户频繁切换页面或者在页面内进行操作,通过合理设置staleTimecacheTime,可以避免频繁地重新获取用户数据,从而提升了用户体验。
  2. AbortController在请求取消中的作用
    • 在数据获取过程中,有时候我们需要在组件卸载或者不再需要获取数据时取消正在进行的网络请求。这时候AbortController就发挥了重要作用。在fetchItems函数中,我们可以接收AbortSignal作为参数传递给fetch操作。当组件的状态发生变化(例如用户导航到其他页面,当前组件不再需要显示相关数据)时,我们可以触发AbortControllerabort方法来取消正在进行的fetch请求。
    • 这样做的好处是避免了资源浪费,特别是在处理长时网络请求或者大量数据请求时。如果不取消这些不必要的请求,可能会导致网络带宽的浪费,同时可能会在请求完成后更新已经不需要更新的组件状态,从而引发潜在的错误。例如,在一个包含多个数据请求的复杂页面中,如果用户快速在不同的视图之间切换,使用AbortController可以确保只有与当前视图相关的数据请求被执行,提高了应用的性能和稳定性。

(六)添加错误边界(Error Boundaries)

1. ErrorBoundary组件代码示例

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;
    }
}
  1. 集成到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组件内,这样就可以有效地捕获FormFilteredItems等子组件可能出现的错误。当子组件发生错误时,ErrorBoundary会将自身的hasError状态设置为true,然后渲染一个错误提示(如<h1>Something went wrong.</h1>),而不是让整个应用崩溃,从而提高了应用的稳定性。

四、总结

通过以上一系列的优化措施,我们对初始的复杂ComplexComponent进行了全面的重构。从最初的性能问题和可维护性较差的状态,逐步构建成为一个高效、可维护的组件。使用useMemo缓存计算结果、提取子组件、使用useCallback减少重渲染、懒加载子组件、优化数据获取以及添加错误边界等技术手段,不仅提高了组件的性能,减少了不必要的资源消耗,还大大提高了代码的可读性和可维护性,使得组件在面对复杂业务逻辑和大规模数据时能够更加稳定和高效地运行。这些优化措施是构建高质量React应用的重要实践经验,可以应用于各种规模和复杂度的React项目中。