likes
comments
collection
share

【译】React.js 中高质量应用的最佳实践和设计模式

作者站长头像
站长
· 阅读数 22

原文:Best Practices and Design Patterns in React.js for High-Quality Applications,适当增删 原作者:Ori Baram

文章已获原文作者授权,禁止转载和商用

1. 构建可扩展的React应用程序最佳实践

1.1 目录结构

不按文件类型对组件进行分组,而是按特征。示例:

/src
├── /components
│   ├── /Header
│   │   ├── Header.js
│   │   └── Header.css
│   ├── /Footer
│   │   └── Footer.js
│   └── /Button
│       ├── Button.js
│       └── Button.css
├── /pages
│   ├── /Homepage
│   │   ├── Homepage.js
│   │   └── Homepage.css
│   └── /ProfilePage
│       ├── ProfilePage.js
│       └── ProfilePage.css
├── /services
│   └── api.js
├── /context
│   └── context.js
└── index.js

1.2 保持组件小而集中

小而集中的组件易于理解,维护和测试。

假设您有一个UserProfile组件代码体积逐渐变大。您可以将其分解为更小的组件,如ProfilePicture、UserName和UserBio。这将使每个组件更易于处理和重用。

// UserProfile.js
import React from 'react';
import ProfilePicture from './ProfilePicture';
import UserName from './UserName';
import UserBio from './UserBio';

const UserProfile = ({ user }) => (
  <div className="user-profile">
    <ProfilePicture src={user.picture} />
    <UserName name={user.name} />
    <UserBio bio={user.bio} />
  </div>
);

export default UserProfile;

1.3 命名规范

当您为组件、props和状态变量指定有意义的名称时,可以帮助其他人(以及将来的您!)更容易地理解您的代码。另外,从长远来看,它使代码更容易维护。这里有一些专业命名建议:

  • 对于组件,使用帕斯卡命名法(如UserProfile.js)
  • 对于变量和函数,使用驼峰命名法(如getUserData())
  • 对于常量,使用大写字母拼接(如API_URL)

1.4 页面(容器组件)和展示组件

页面处理从外部源获取数据等任务(如api),管理状态和逻辑,并使用props将数据传递给展示组件。同时,展示组件负责呈现UI元素并显示从父组件传递下来的数据。通过分离这些职责,我们可以创建更加模块化和可重用的组件。

示例 — TodoApp 页面:

// components/TodoApp.js (容器组件)
import React, { useState } from "react";
import TodoList from "./TodoList";

function TodoApp({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);

  const handleToggle = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  return <TodoList todos={todos} onToggle={handleToggle} />;
}

export default TodoApp;

TodoList组件:

// components/TodoList.js (展示组件)
function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => onToggle(todo.id)}>
          {todo.completed ? <s>{todo.text}</s> : todo.text}
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

1.5 使用数组映射保持代码清爽

在React组件中使用数组并对其进行映射是一种避免代码重复的巧妙技巧。假设您正在构建一个包含多个链接的导航栏,每个链接都有自己的标题、路径和图标。您不必为每个链接编写相同的结构和代码,而可以创建一个对象数组,其中包含所有必要的数据,并使用map函数动态呈现它们。 看看这个示例代码,它演示了如何轻松地创建链接数组并映射它们以呈现导航栏:

const links = [
  { title: 'Home', path: '/home', icon: <HomeIcon /> },
  { title: 'About', path: '/about', icon: <AboutIcon /> },
  { title: 'Contact', path: '/contact', icon: <ContactIcon /> }
];

function Navbar() {
  return (
    <nav>
      <ul>
        {links.map((link) => (
          <li key={link.path}>
            <Link to={link.path}>
              {link.icon}
              <span>{link.title}</span>
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

在上面的例子中,我们演示了如何在React组件中创建项目数组并对其进行映射,从而避免代码重复。这种技术不仅适用于呈现导航栏,而且适用于呈现带有多个输入字段的表单。

通过创建包含每个输入字段所需数据的对象数组,您可以对它们进行映射以动态呈现输入字段。这可以极大地简化代码并使其更易于维护,特别是在处理具有许多输入字段的表单时。

示例:

const inputs = [
  { label: 'First Name', type: 'text', name: 'firstName' },
  { label: 'Last Name', type: 'text', name: 'lastName' },
  { label: 'Email', type: 'email', name: 'email' }
];

function Form() {
  return (
    <form>
      {inputs.map((input) => (
        <div key={input.name}>
          <label htmlFor={input.name}>{input.label}</label>
          <input type={input.type} name={input.name} id={input.name} />
        </div>
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

这个很酷的例子展示了如何在React中使用一个input对象数组来创建动态输入字段。每个输入对象包括每个输入字段的标签、类型和名称。通过使用map()函数,您可以轻松地遍历数组,并使用来自每个input对象的数据动态呈现每个input字段。

使用数组和映射是在React组件中创建动态内容的好方法,而不用一遍又一遍地重复相同的代码。这种技术使您的组件更具可拓展性、可重用性和更易于维护。

2.关注点分离

确保你的应用的每个部分都有一个工作要做。当涉及到React.js时,将逻辑与视图分离可以帮助您的代码更易于维护、可重用和可扩展。

2.1 自定义Hook

自定义hooks允许您从组件中取出有状态逻辑,并跨多个组件使用它。自定义hook只是具有特殊命名约定的JavaScript函数——它们必须以“use”开头(如useForm)。这些hooks可以使用其他内置或自定义hook,它们可以导出一个对象或一个数组,其中包含您想在组件中使用的所有属性和方法。

示例:表单输入处理的自定义Hook

首先,我们使用useState创建一个values对象来保存所有表单输入值。然后,我们创建一个handleChange方法,该方法在输入发生变化时更新values对象。最后,我们添加一个resetForm方法,该方法将所有内容设置为从头开始。

通过在数组中抛出values、handleChange和resetForm,我们可以在所有组件中使用这些属性和方法,并使我们的表单组件方式更容易维护和重用。

// hooks/useForm.js
import { useState } from "react";

function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues({ ...values, [name]: value });
  };

  const resetForm = () => {
    setValues(initialValues);
  };

  return [values, handleChange, resetForm];
}

export default useForm;

现在,我们可以在表单组件中使用useForm自定义hook了:

// components/AuthForm.js
import React from "react";
import useForm from "../hooks/useForm";

function AuthForm({ isLogin }) {
  const initialValues = { username: "", email: "", password: "" };
  const [values, handleChange, resetForm] = useForm(initialValues);

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(values);
    resetForm();
  };

  const inputs = [
    {
      label: 'User name',
      type: "text",
      name: "username",
      value: values.username,
      onChange: handleChange,
    },
    {
      label: 'Email',
      type: "email",
      name: "email",
      value: values.email,
      onChange: handleChange,
    },
    {
      label: 'Password',
      type: "password",
      name: "password",
      value: values.password,
      onChange: handleChange,
    }
  ];

  return (
    <form onSubmit={handleSubmit}>
      {inputs.map((input) => {
        const { label, type, name, value, onChange } = input;

        if (isLogin && label === 'User name') return null;

        return (
          <>
            <label htmlFor="username">{label}</label>
            <input
              type={type}
              name={name}
              value={value}
              onChange={onChange}
            />
          </>
        );
      })}
      <button type="submit">Register</button>
    </form>
  );
}

export default AuthForm;

另一个例子:用于获取数据的自定义Hook

我们所需要做的就是创建一个名为useFetch的自定义hook来处理与获取数据相关的状态和副作用。

// hooks/useFetch.js
import { useState, useEffect } from "react";

function useFetch(url, options) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(url, options);
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (error) {
        setError(error);
      } finally {
        setIsLoading(false);
      }
    };
    fetchData();
  }, [url, options]);

  return { data, error, isLoading };
}

export default useFetch;

现在,我们可以在任何需要API数据的组件中使用useFetch自定义hook:

// components/UserList.js
import React from "react";
import useFetch from "../hooks/useFetch";

function UserList() {
  const { data, error, isLoading } = useFetch("https://api.example.com/users");

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

使用useFetch,我们可以将所有与获取数据相关的状态和副作用保存在一个整洁的位置,然后在多个组件中使用结果数据。这种关注点分离意味着我们可以很容易地更改数据获取逻辑,而不会干扰依赖于它的组件。

2.2 服务

如果你想让你的React应用更加模块化,使用服务是一个很好的方法。从根本来说,服务就是处理业务逻辑的函数或类,比如调用API、处理数据或其他有用的任务。您可以导入这些服务,并在组件或自定义hook中使用它们。这是一种简单的方法,可以将逻辑从视图中分离出来,并保持事物的组织性。

示例:API服务

假设我们需要使用RESTful API对用户数据执行一些CRUD操作。为了更简单,我们可以创建一个服务来处理。这是一种方便的与API通信并有效完成任务的方法。

// services/apiService.js
const API_URL = 'https://api.example.com/users';

async function sendRequest(url, options) {
  const response = await fetch(url, options);
  return await response.json();
}

function createRequestOptions(method, body) {
  return {
    method,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  };
}

async function fetchUsers() {
  return await sendRequest(API_URL);
}

async function createUser(user) {
  const options = createRequestOptions('POST', user);
  return await sendRequest(API_URL, options);
}

async function updateUser(userId, user) {
  const options = createRequestOptions('PUT', user);
  return await sendRequest(`${API_URL}/${userId}`, options);
}

async function deleteUser(userId) {
  const options = createRequestOptions('DELETE');
  await sendRequest(`${API_URL}/${userId}`, options);
}

export default {
  fetchUsers,
  createUser,
  updateUser,
  deleteUser,
};

现在,我们可以在组件或自定义hook中使用API服务了:

// components/UserList.js
import { useEffect, useState } from "react";
import apiService from "../services/apiService";

function UserList() {
  const [users, setUsers] = useState([]);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const data = await apiService.fetchUsers();
        setUsers(data);
        setError(null);
      } catch (error) {
        setError(error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

通过API服务,我们可以将所有API交互逻辑与UserList组件中的视图分开。

示例:公共服务

假设我们需要做一些有用的事情,比如格式化日期、计算总和或制作唯一ID。我们可以创建一个便利的公共服务来处理这一切。它就像我们自己的小帮手,当我们需要完成一些有用的工作时,我们可以调用它。

// services/utilityService.js
function formatDate(date, format) {
  // 格式化日期逻辑
}

function calculateSum(numbers) {
  return numbers.reduce((sum, number) => sum + number, 0);
}

function generateUUID() {
  // 生成UUID逻辑
}

export default { formatDate, calculateSum, generateUUID };

现在我们可以很容易地在组件或自定义hook中使用我们的公共服务,以使我们的代码更高效和可重用

// components/TransactionList.js
import React from "React";
import utilityService from "../services/utilityService";

function TransactionList({ transactions }) {
  const totalAmount = utilityService.calculateSum(
    transactions.map((transaction) => transaction.amount)
  );

  return (
    <div>
      <ul>
        {transactions.map((transaction) => (
          <li key={transaction.id}>
            {transaction.description} - {utilityService.formatDate(transaction.date, "MM/DD/YYYY")}
          </li>
        ))}
      </ul>
      <div>Total Amount: {totalAmount}</div>
    </div>
  );
}

export default TransactionList;

通过使用公共服务,我们将公共逻辑与视图分开,从而使TransactionList组件更易于管理和重用。

什么时候使用自定义钩子?什么时候使用服务功能?

在决定是使用自定义hook还是服务函数时,重要的是要考虑应用程序的特定用例和需求。当您需要管理状态或上下文时,或者当您希望跨多个组件重用功能时,自定义hook更合适。另一方面,服务功能更适合于不依赖于状态或上下文的可重用逻辑。

自定义Hook:

  • 管理复杂状态: 如果您需要跨多个组件管理复杂的状态,那么自定义hook是封装逻辑并使其可重用的好方法。例如,如果您需要管理用户的设置并将其与您的产品进行匹配,那么像useUserSettings这样的自定义hook可以帮助跟踪用户的设置,并提供检查与产品匹配的功能。自定义hook利用useState来跟踪用户的设置以及与产品的匹配,并提供一个函数来根据用户的设置和产品的属性检查匹配情况。
  • 上下文相关的功能: 自定义hook可以帮助从应用程序访问上下文,并提供可重用的功能。例如,如果您需要认证用户身份并跨多个组件管理用户的状态,那么像useLoggedUser这样的自定义hook可以帮助您访问用户的状态并提供更新状态的功能。自定义hook可以利用LoggedUserContext来访问用户的状态,并提供设置新用户、设置现有用户、更新用户状态等功能。

服务功能:

  • 特殊任务功能: 如果您需要执行不依赖于上下文或状态的特定任务,那么服务功能可能更合适。例如,如果需要验证电子邮件地址或密码等数据,或格式化货币值或日期等数据,或对产品列表或搜索结果等数据进行排序,服务功能可以提供这些功能,而不需要状态或上下文。
  • 独立功能: 如果您需要提供可跨多个应用程序或平台使用的功能,那么服务功能是封装该逻辑并使其可重用的好方法。例如,如果您需要提供图像处理、文件存储、电子邮件发送或支付处理的功能,那么服务功能可以以跨多个应用程序或平台使用的方式提供这些功能。

3. 组件设计模式

在本节中,我们将查看一些可以帮助您创建灵活且可重用组件的设计模式。这些模式将帮助您将大型组件划分为更小、更易于管理的部分,使它们更容易理解、测试和维护。

3.1 高阶组件(HOCs)

高阶组件(HOCs) 是接受一个组件并返回一个具有更多特性或行为的新组件的函数。它们允许您重用组件逻辑和处理诸如身份验证或数据获取之类的事情。

例如,您可以使用HOC来检查用户是否经过身份验证,然后将需要身份验证的组件封装在其中。如果用户通过了身份验证,将显示包裹的组件。但如果没有,将被重定向到登录页面。示例:

function requireAuth(Component) {
  return function AuthenticatedComponent(props) {
    const isAuthenticated = checkAuth();

    if (!isAuthenticated) {
      return <Redirect to="/login" />;
    }

    return <Component {...props} />;
  };
}

export default requireAuth;

在上面的例子中,我们得到requireAuth函数,它获取一个组件作为输入并返回一个新的组件去检查用户是否登录。如果没有,则重定向到登录页。否则,将使用来自HOC的额外props渲染包裹的组件。

现在,我们也可以使用HOC来包装一些需要获取数据的组件。基本上,HOC会获取数据并将其作为props传递给组件。

示例:

function withDataFetching(Component) {
  return function DataFetchingComponent(props) {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
      setIsLoading(true);

      fetch(props.url)
        .then((response) => response.json())
        .then((data) => {
          setIsLoading(false);
          setData(data);
        })
        .catch((error) => {
          setIsLoading(false);
          setError(error);
        });
    }, [props.url]);

    return (
      <Component {...props} data={data} isLoading={isLoading} error={error} />
    );
  };
}

export default withDataFetching;

在上面的例子中,withDataFetching函数接受一个组件,从URL获取数据,并将其作为props传递下去。如果有错误,它会传递错误prop,如果数据还在加载,isLoading prop会被设为true。

你也可以在React中使用很多其他的HOC!其中之一是memomemo允许你优化你的组件,使它只在收到的props发生变化时才重新渲染,使你的应用程序更高效,提高性能。

但是在流行的React库中也有一些常用的其他HOC。例如,来自react-redux库的connect,它将组件连接到Redux store,这样它就可以访问store的state和dispatch functions。这意味着您可以轻松地创建对Redux store中的变化做出反应的组件。另一个是来自react-redux-firebase库的withFirebase,它为组件提供了一个Firebase实例,这样它就可以与Firebase后端交互。

HOC允许你重用组件逻辑并处理影响整个应用的问题。让你的组件整洁美观,集中,容易维护。

3.2 Render props

Render props允许你通过将函数作为prop传递给子组件来在组件之间共享代码,并且在某些情况下它比HOC更灵活。

当调用这个函数时,返回一个React元素(通常是JSX)。渲背后的思想是让父组件负责渲染组件的某些部分。因此,子组件可以专注于提供必要的功能,而父组件决定呈现的内容应该是什么样子。

假设你正在构建一个在线商店应用程序,你需要在应用程序的不同部分显示产品列表。你希望获取产品并以不同的方式显示它们,例如网格视图或列表视图。使用render props模式来实现:

首先,创建一个ProductFetcher组件来获取产品数据并接受一个render prop(一个函数):

import React, { useState, useEffect } from 'react';

const ProductFetcher = ({ render }) => {
  const [products, setProducts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Fetch products from API (using a service or directly making an API call)
    fetchProducts().then((fetchedProducts) => {
      setProducts(fetchedProducts);
      setIsLoading(false);
    });
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return render(products);
};

export default ProductFetcher;

在本例中,ProductFetcher组件负责获取产品数据并管理加载状态。它接受一个render prop(一个函数),并将产品数据作为参数调用它。

现在,让我们创建两个独立的组件,用于在网格视图和列表视图中展示产品:

// ProductGrid.js
import React from 'react';

const ProductGrid = ({ products }) => {
  return (
    <div className="product-grid">
      {products.map((product) => (
        <div key={product.id} className="product-grid-item">
          <img src={product.image} alt={product.name} />
          <h2>{product.name}</h2>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  );
};

export default ProductGrid;
// ProductList.js
import React from 'react';

const ProductList = ({ products }) => {
  return (
    <ul className="product-list">
      {products.map((product) => (
        <li key={product.id} className="product-list-item">
          <img src={product.image} alt={product.name} />
          <h2>{product.name}</h2>
          <p>${product.price}</p>
        </li>
      ))}
    </ul>
  );
};

export default ProductList;

最后,使用ProductFetcher组件并传递适当的render函数,以不同的格式显示产品:

import React from 'react';
import ProductFetcher from './ProductFetcher';
import ProductGrid from './ProductGrid';
import ProductList from './ProductList';

const App = () => {
  return (
    <div>
      <h1>Grid View</h1>
      <ProductFetcher render={(products) => <ProductGrid products={products} />} />

      <h1>List View</h1>
      <ProductFetcher render={(products) => <ProductList products={products} />} />
    </div>
  );
};

export default App;

在这个例子中,我们有一个ProductFetcher组件来获取和管理产品数据。ProductGrid和ProductList组件负责以不同的格式展示产品。在render prop的帮助下,ProductFetcher组件获取产品数据,父组件(在本例中为App)决定如何展示产品。

通过使用render prop模式,我们获得了一些好处,比如:

  1. 关注点分离:ProductFetcher组件只负责获取和管理产品的加载状态。它不必担心产品的展示方式。
  2. 可重用性:ProductFetcher组件可以在整个应用程序中使用,以不同的格式获取和显示产品,而不会有太多麻烦。
  3. 灵活性:父组件可以决定产品应该如何显示,这给了我们自定义UI的自由,而不需要修改获取逻辑。

3.3 复合组件

复合组件是一种很酷的设计技巧,可以帮助您创建更好、更灵活的组件。您可以将相关组件组合在一起,并从父组件控制它们的行为,这可以使您的代码更加有组织和用户友好。

一个流行的例子是手风琴组件,它通常有许多面板,当您单击它们时,它们会展开和折叠。使用复合组件,您可以构建一个可重用的手风琴组件,该组件既易于定制,又易于使用。

示例:

function Accordion({ children }) {
  const [selectedIndex, setSelectedIndex] = useState(0);

  return (
    <div>
      {React.Children.map(children, (child, index) => {
        if (child.type.name === 'AccordionHeader') {
          return React.cloneElement(child, {
            key: index,
            isOpen: index === selectedIndex,
            onClick: () => setSelectedIndex(index),
          });
        }
        if (child.type.name === 'AccordionContent') {
          return React.cloneElement(child, {
            key: index,
            isOpen: index === selectedIndex,
          });
        }
        return null;
      })}
    </div>
  );
}

export default Accordion;
function AccordionContent({ isOpen, children }) {
  return isOpen ? <div>{children}</div> : null;
}

export default AccordionContent;
function AccordionHeader({ isOpen, onClick, children }) {
  return (
    <div onClick={onClick}>
      <h3>{children}</h3>
      {isOpen ? <i>-</i> : <i>+</i>}
    </div>
  );
}

export default AccordionHeader;

在这个例子中,我们得到了这个手风琴组件,在这个组件中,我们呈现了一列子组件,并用React.Children.map遍历它们,以决定何时显示它们。我们使用React.cloneElement给AccordionHeader和AccordionContent一些额外的props来控制它们的行为。

下面是所有这些组件在MyAccordion组件中的样子:

import { useState } from 'react';
import Accordion from './Accordion';
import AccordionHeader from './AccordionHeader';
import AccordionContent from './AccordionContent';

function MyAccordion({ items }) {
  const [selectedIndex, setSelectedIndex] = useState(0);

  return (
    <Accordion>
      {items.map((item, index) => (
        <div key={index}>
          <AccordionHeader
            isOpen={selectedIndex === index}
            onClick={() => setSelectedIndex(index)}
          >
            {item.header}
          </AccordionHeader>
          <AccordionContent isOpen={selectedIndex === index}>
            {item.content}
          </AccordionContent>
        </div>
      ))}
    </Accordion>
  );
}

export default MyAccordion;

如果这对你来说还不够,我们还有一个标签组件,当你点击不同的标签时它会显示不同的内容。就像手风琴一样,我们可以使用复合组件来简化它的构建和使用。

示例:

function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div>
        {React.Children.map(children, (child, index) => {
          if (child.type.name === 'Tab') {
            return React.cloneElement(child, {
              key: index,
              isActive: index === activeTab,
              onClick: () => setActiveTab(index),
            });
          }
          return null;
        })}
      </div>
      {React.Children.map(children, (child, index) => {
        if (child.type.name === 'TabContent') {
          return React.cloneElement(child, {
            key: index,
            isActive: index === activeTab,
          });
        }
        return null;
      })}
    </div>
  );
}

export default Tabs;
function Tab({ isActive, onClick, children }) {
  return (
    <div onClick={onClick}>
      <h3 style={{ color: isActive ? 'red' : 'black' }}>{children}</h3>
    </div>
  );
}

export default Tab;
function TabContent({ isActive, children }) {
  return isActive ? <div>{children}</div> : null;
}

export default TabContent;

在本例中,我们获得了一个Tabs组件,它显示不同内容的选项卡列表。我们使用React.cloneElement给Tab和TabContent组件一些额外的props来控制它们的行为。

下面是它们组合在一起的样子:

import { useState } from 'react';
import Tabs from './Tabs';
import Tab from './Tab';
import TabContent from './TabContent';

function MyTabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <Tabs>
      {tabs.map((tab, index) => (
        <Tab
          key={index}
          isActive={activeTab === index}
          onClick={() => setActiveTab(index)}
        >
          {tab.title}
        </Tab>
      ))}
      {tabs.map((tab, index) => (
        <TabContent key={index} isActive={activeTab === index}>
          {tab.content}
        </TabContent>
      ))}
    </Tabs>
  );
}

export default MyTabs;

复合组件可以让你的组件超级灵活和富有表现力。您可以将它们组合在一个父组件中,而不是仅仅拥有一堆独立的组件,从而使事情更加直观和用户友好。像手风琴、标签和下拉菜单这样的功能在web应用程序中可能会变得相当复杂,但使用复合组件,你可以简化一切。

3.4 使用React.memo优化渲染

我们已经提到的React.memo,就像是优化功能组件的秘密武器。它确保组件只在props改变时才重新渲染,这可能是一个巨大的性能提升,特别是当你处理复杂或昂贵的计算props时。基本上,它可以帮助你避免不必要的渲染,并保持你的应用程序平稳运行。

示例:

import React from 'react';

const UserProfileName = ({ name }) => (
  <h2 className="user-profile-name">{name}</h2>
);

export default React.memo(UserProfileName);

在这个例子中,UserProfileName组件只会在它的name prop改变时重新渲染,从而避免了不必要的更新并提高了性能。

3.5 使用React.lazy实现代码分割和懒加载

React.lazy 是一个非常简洁的工具,可以帮助你加快应用程序的速度,提高效率。这是一个内置功能,允许您拆分组件并仅在实际需要时加载它们。如果你有一个非常大的应用程序,有很多不是立即需要的组件,就可以使用它。通过React.lazy,您可以选择加载的组件以及加载的确切时间。这意味着你的应用程序可以启动得更快,使用更少的内存,并且通常效率更高。另外,它非常容易使用—————你所需要做的就是把你的组件包装在一个特殊的函数中。

假设你正在用React构建一个电子商务应用程序。您有一个产品列表页面,其中显示了库存中的所有产品。每个产品都有图像、名称、价格和描述。

您还有一个产品详细信息页面,其中显示关于单个产品的更多信息,例如评论、评分和相关产品。

如果你在产品列表页面上加载产品详细信息页面的所有代码,它可能会减慢初始加载时间,并导致应用程序感觉迟钝。这时候React.lazy派上用场。

下面是如何使用React.lazy只在用户点击产品时才懒加载ProductDetails页面:

首先,为ProductDetails组件创建一个单独的文件:

import React from "react";

function ProductDetails(props) {
  return (
    <div>
      <h1>{props.product.name}</h1>
      <img src={props.product.imageUrl} alt={props.product.name} />
      <p>{props.product.description}</p>
      <p>Price: {props.product.price}</p>
      <p>Rating: {props.product.rating}</p>
      {/* other details */}
    </div>
  );
}

export default ProductDetails;

接下来,将ProductDetails组件封装在对React.lazy的调用中:

const ProductDetails = React.lazy(() => import("./ProductDetails"));

这告诉React只在需要时才懒加载ProductDetails组件。

最后,在产品列表页面中使用ProductDetails组件,并将其包装在一个Suspense组件中以处理加载状态:

import React, { Suspense } from "react";

const ProductDetails = React.lazy(() => import("./ProductDetails"));

function ProductListing(props) {
  const [selectedProduct, setSelectedProduct] = useState(null);

  function handleProductClick(product) {
    setSelectedProduct(product);
  }

  return (
    <div>
      <h1>Product Listing Page</h1>
      {props.products.map((product) => (
        <div key={product.id} onClick={() => handleProductClick(product)}>
          <h2>{product.name}</h2>
          <img src={product.imageUrl} alt={product.name} />
          <p>Price: {product.price}</p>
        </div>
      ))}
      {selectedProduct && (
        <Suspense fallback={<div>Loading...</div>}>
          <ProductDetails product={selectedProduct} />
        </Suspense>
      )}
    </div>
  );
}

export default ProductListing;

当用户单击一个产品时,handleProductClick函数将selectedProduct状态设置为所单击的产品。如果selectedProduct不为空,ProductDetails组件将在Suspense组件中呈现。

Suspense组件显示一个加载状态,直到ProductDetails组件加载完。

3.6 使用Context和React.createContext管理全局状态

Context API是在多个组件之间共享状态的好方法,而不必通过组件树向下传递props。

示例:

import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const useTheme = () => useContext(ThemeContext);

export { ThemeProvider, useTheme };

现在,我们有一个ThemeContext和一个ThemeProvider组件包裹app。这样,所有组件都可以使用useTheme钩子来访问和更新theme,而不用像烫手山芋一样传递它。

示例:

import React from 'react';
import { useTheme } from './ThemeContext';

const ThemeToggle = () => {
  const { theme, setTheme } = useTheme();

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <button onClick={toggleTheme}>
      Switch to {theme === 'light' ? 'dark' : 'light'} theme
    </button>
  );
};

export default ThemeToggle;

3.7 管理复杂状态的Reducers

Reducers 就像一个超级方便的工具,可以帮助您以可预测和可测试的方式管理复杂的状态。它们是一种函数式编程模式,可以很好地与useReducer hook或Redux之类的库一起工作。通常,你传递给它们当前状态和一个动作,它们返回新状态。

示例:Todo App

首先:创建一个函数它将是reducer和你想要的actions:

// todoReducer.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';

export const todoReducer = (state, action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, { id: Date.now(), text: action.text, completed: false }];
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
};

export default todoReducer;

然后,你可以使用todoReducer和useReducer钩子:

// TodoApp.js
import React, { useReducer } from 'react';
import { todoReducer, ADD_TODO, TOGGLE_TODO } from './todoReducer';

const TodoApp = () => {
  const [todos, dispatch] = useReducer(todoReducer, []);

  const addTodo = text => {
    dispatch({ type: ADD_TODO, text });
  };

  const toggleTodo = id => {
    dispatch({ type: TOGGLE_TODO, id });
  };

  // Render the todos and form for adding new todos
};

export default TodoApp;

3.8 渲染列表时使用key

当你在React中渲染一个列表时,确保给每个项目一个唯一的key:

import React from 'react';

const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo => (
      <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
        {todo.completed ? <s>{todo.text}</s> : todo.text}
      </li>
    ))}
  </ul>
);

export default TodoList;

在这个例子中,我们给todos数组中的每个元素一个唯一的key,这有助于React更快地渲染,避免弄乱DOM,优化渲染,避免不必要的DOM更新。

通过坚持这些最佳实践,你将能够创建更容易管理、扩展和保持平稳运行的React应用程序。通过将组件分解成更小、更集中的组件,优化渲染,只加载你真正需要的东西,以及有效地处理状态,你将创建一个一流的React应用程序。所以,当你处理自己的React项目时,一定要记住这些技巧,以确保它们能长期持续下去。

结论

总而言之,我们所讨论的所有很酷的设计模式和最佳实践对于制作出色的React.js应用程序都非常重要。如果你想编写干净、易于阅读、易于维护和扩展的代码,那么你必须正确命名和放置文件夹,将组件拆分为自定义钩子服务展示组件,使用合适的名称,并使用HOCrender props复合组件reducer。不要忘记基本的东西,比如保持你的组件代码少,用React.memo优化你的渲染,用React.lazy拆分代码。使用Content API,渲染列表时使用key。遵循所有这些提示和技巧,从而开发你、其他开发人员和你的用户都喜欢的健壮而高效的应用程序。

本文中提到的所有代码都可以在这个库中找到。