精选|如何快速理解 React 设计模式

在深入了解 React 设计模式的细节之前,我们应该先了解它们是什么以及为什么需要它们。简单地说,设计模式是针对常见开发问题的可重复解决方案。

【参考文献】

文章:《Understanding Design Patterns in React》

作者:Roopal Jasnani

上述译文仅供参考,具体内容请查看上面链接,解释权归原作者所有。

【关于TalkX】

TalkX是一款基于GPT实现的IDE智能开发插件,专注于编程领域,是开发者在日常编码中提高编码效率及质量的辅助工具,TalkX常用的功能包括但不限于:解释代码、中英翻译、性能检查、安全检查、样式检查、优化并改进、提高可读性、清理代码、生成测试用例、有趣的图表生成以及语音助手托克斯等。

TalkX建立了全球加速网络,不需要考虑网络环境,响应速度快,界面效果和交互体验更流畅。并为用户提供了Open AI的密钥,不需要自备ApiKey,不需要自备账号,不需要魔法。

TalkX产品支持:JetBrains (包括 IntelliJ IDEA、PyCharm、WebStorm、Android Studio、HBuilder、VS Code、Goland)

React 可以说是用于构建用户界面的最流行的 JavaScript 库,其原因之一就是它的无主观性。可重用组件、优秀的开发人员工具和广泛的生态系统是 React 最受欢迎的一些特性。然而,除了功能和社区支持外,React 还提供并实现了一些广泛使用的设计模式,从而进一步简化了开发过程。

在深入了解 React 设计模式的细节之前,我们应该先了解它们是什么以及为什么需要它们。简单地说,设计模式是针对常见开发问题的可重复解决方案。它们是一个基本模板,您可以在此基础上根据给定的要求构建任何功能,同时遵循最佳实践。我们可以利用它们来节省开发时间,减少编码工作,因为它们是标准术语,是已知问题的预测试解决方案。

让我们开始吧!

条件渲染

这无疑是 React 组件中最基本、使用最广泛的模式之一(或许也无需过多介绍)。经常需要根据特定条件呈现或不呈现特定的 JSX 代码。这可以通过条件呈现来实现。例如,我们希望向未认证用户显示 "登录 "按钮,向已登录用户显示 "注销 "按钮。

通常情况下,条件渲染是通过&&运算符或ternary运算符来实现的。

less 复制代码
{condition && <span>Rendered when `truthy`</span>}
{condition ? <span>Rendered when `truthy`</span> : <span>Rendered when `falsy`</span>}

在某些情况下,我们还可以考虑使用ifswitch或对象字面形式。

自定义钩子

事实证明,React 挂钩是与功能组件相结合的革命性引入。它们提供了一种简单而直接的方式来访问 props, state, context, refs和生命周期等常见的 React 功能。我们可能满足于使用传统的钩子,但还有更多。让我们来了解一下引入自定义钩子的好处。想想你为一个组件编写的逻辑,你可能使用了基本的钩子,如 useEffectuseState。一段时间后,需要在另一个新组件中使用相同的逻辑。虽然复制可能是最快速、最简单的方法,但自定义钩子来实现同样的效果会更有趣。在钩子中提取通常需要的逻辑可以使代码更简洁,提高可重用性,当然还有可维护性。

从一个常见的用例开始,在不同的组件中调用 API。例如,一个组件从应用程序接口获取数据后,会渲染用户列表。

scss 复制代码
const UsersList = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      const res = await fetch("https://jsonplaceholder.typicode.com/users");
      const response = await res.json();
      setData(response.data);
    } catch (error) {
      setError(error);
      setLoading(false);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return (...);
};

既然 API 调用是大多数组件的支柱,为什么不将其提取到一个地方呢?可以在新的useFetch钩子中轻松提取这一功能:

scss 复制代码
export const useFetch = (url, options) => {
  const [data, setData] = useState();
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      const res = await fetch(url, options);
      const response = await res.json();
      setData(response.data);
    } catch (error) {
      setError(error);
      setLoading(false);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, error, loading, refetch: fetchData };
};

const UsersList = () => {
  const { data, error, loading, refetch } = useFetch(
    "https://jsonplaceholder.typicode.com/users"
  );

  return (...);
};

我想到的自定义钩子的其他一些可能用例包括:

● 获取窗口尺寸

● 访问和设置本地存储

● 在布尔状态之间切换等。

提供商模式

Prop drilling 是 React 开发人员面临的一个主要问题。道具钻取是指数据(props)向下传递到不同的组件,直到需要道具的组件为止。当某些数据需要传递到组件树深处的一个或多个嵌套组件时,这很容易成为一个问题,因为会建立一个看似不必要的数据传递链。

这时,Provider 模式就派上用场了。提供程序模式允许我们在一个中心位置存储数据(全局数据或可共享数据)。然后,上下文提供者/存储可以将这些数据直接传递给任何需要的组件,而无需钻取道具。React 内置的上下文 API 就是基于这种方法。使用这种模式的其他一些库包括react-reduxfluxMobX等。

要理解这一点,可以举一个例子,在应用程序中实现明/暗主题就是一个常见的场景。如果不使用 Provider 模式,我们的实现将是这样的:

javascript 复制代码
const App = ({ theme }) => {
  return (
    <>
      <Header theme={theme} />
      <Main theme={theme} />
      <Footer theme={theme} />
    </>
  );
};

const Header = ({ theme }) => {
  return (
    <>
      <NavMenu theme={theme} />
      <PreferencesPanel theme={theme} />
    </>
  );
};

让我们看看引入上下文 API 是如何简化事情的。

javascript 复制代码
const ThemeContext = createContext("light", () => "light");

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
export { ThemeContext, ThemeProvider };

const App = () => {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
};
const PreferencesPanel = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  ...
};

这样不是更好吗?Provider 模式的其他可能用途包括:

● 验证状态管理

● 管理本地/语言选择偏好等

高阶组件模式

React 中的高阶组件是一种在组件中重用逻辑的高级技术。它是根据 React 的组成特性创建的一种模式。它本质上结合了编程的 "不要重复自己"(DRY)原则。与 JS 中的高阶函数类似,HOC 是一种纯函数,它将组件作为参数,并返回一个增强和升级的组件。它符合 React 功能组件的本质,即重构而轻继承。现实世界中的一些例子包括

react-redux: connect(mapStateToProps, mapDispatchToProps)(UserPage)react-router: withRouter(UserPage)material-ui: withStyles(styles)(UserPage)

举例来说,一个简单的组件可以渲染用户列表并处理加载、出错和无可用数据等各种状态。

javascript 复制代码
const UsersList = ({ hasError, isLoading, data }) => {
  const { users } = data;
  if (isLoading) return <p>Loading...</p>;
  if (hasError) return <p>Sorry, data could not be fetched.</p>;
  if (!data) return <p>No data found.</p>;

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

const { data, loading, error } = fetchData();
<UsersList {...{ data, error }} isLoading={loading} />;

显示这种不同的 API 抓取状态是一种常见的逻辑,可以很容易地在许多组件中重复使用。因此,要在 HOC 中实现这一点,我们可以这样做

javascript 复制代码
const withAPIFeedback =
  (Component) =>
  ({ hasError, isLoading, data }) => {
    if (isLoading) return <p>Loading...</p>;
    if (hasError) return <p>Sorry, data could not be fetched.</p>;
    if (!data) return <p>No data found.</p>;
    return <Component {...{ data }} />;
  };

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

const { data, loading, error } = fetchData();
const UsersListWithFeedback = withAPIFeedback(UsersList);
<UsersListWithFeedback {...{ data, error }} isLoading={loading} />;

在处理跨领域问题时,特别是我们希望在整个应用程序中重复使用组件逻辑时,HOC 非常有用。

一些可能的用法如下:

● 实施日志记录机制。

● 管理授权等。

呈现与容器组件模式

顾名思义,这种方法就是将组件分为两种不同的类别和实施策略:

呈现组件: 这些组件本质上是纯粹的无状态功能组件。它们关注的是事物的外观。它们与应用程序的任何部分都没有任何依赖关系,用于显示数据。

容器组件: 与展示组件不同,容器组件更多的是负责工作方式。它们充当任何副作用、有状态逻辑和呈现组件本身的容器。

通过这种方法,我们可以更好地分离关注点(因为我们不会只有一个复杂的组件来处理所有的呈现和逻辑状态)。此外,这种方法还能更好地重用呈现组件(因为它们不存在任何依赖关系,因此可以轻松地在多个场景中重用)。

因此,作为开发人员,即使没有必须重用特定组件的直接场景,也应该以创建无状态组件为目标。对于组件的层次结构,最好的做法是让父组件尽可能多地保留状态,并创建无状态的子组件。

举例来说,任何渲染列表的组件都可以是呈现组件:

javascript 复制代码
const ProductsList = ({ products }) => {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
};

与此相对应的容器组件可以是:

ini 复制代码
const ProductsCatalog = () => {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetchProducts();
  }, []);

  return <ProductsList {...{ products }} />;
};

受控与非受控组件模式

网络表单是大量应用中的常见需求。在 React 中,有两种方法可以在组件中处理表单数据。第一种方法是在组件中使用 React 状态来处理表单数据。这就是所谓的受控组件。第二种方法是让 DOM 在组件中自行处理表单数据。这就是所谓的非受控组件。"不受控 "指的是这些组件不受 React 状态控制,而是由传统的 DOM 突变控制。

为了更好地理解这些组件,让我们从非受控组件的示例开始。

ini 复制代码
function App() {
  const nameRef = useRef();
  const emailRef = useRef();

  const onSubmit = () => {
    console.log("Name: " + nameRef.current.value);
    console.log("Email: " + emailRef.current.value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" ref={nameRef} required />
      <input type="email" name="email" ref={emailRef} required />
      <input type="submit" value="Submit" />
    </form>
  );
}

在这里,我们使用 ref 访问输入。这种方法可以在需要时从字段中提取值。现在让我们看看这个表单的受控版本会是什么样子:

ini 复制代码
function App() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const onSubmit = () => {
    console.log("Name: " + name);
    console.log("Email: " + email);
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        type="text"
        name="name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        required
      />
      <input
        type="email"
        name="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <input type="submit" value="Submit" />
    </form>
  );
}

在这里,输入的值始终由 React 状态驱动。这种流程会将值的变化推送到表单组件,因此表单组件始终拥有输入的当前值,而无需明确询问。虽然这意味着您需要输入更多代码,但您现在也可以将值传递给其他 UI 元素,或者使用道具和事件回调从其他事件处理程序中重置值。

React 表单支持受控和不受控组件。在某些用例中,我们可能需要处理简单的用户界面和反馈,这时我们可能会发现最好采用不受控组件。对于复杂的逻辑,我们强烈建议使用受控组件。

渲染道具模式

根据 React 的官方文档,渲染道具(Render Prop)指的是一种使用道具(prop)在组件间共享代码的技术,道具的值是函数。与 HOC 类似,Render Props 也具有相同的目的:通过在组件之间共享有状态逻辑来处理交叉问题。

实施渲染道具设计模式的组件会将返回 React 元素的函数作为道具,然后调用它,而不是使用其渲染逻辑。因此,我们可以使用函数 prop 来决定呈现什么,而不是在每个组件内部硬编码逻辑。

为了更好地理解这一点,让我们举一个例子。假设我们有一个产品列表,需要在应用程序的不同位置呈现。这些位置的用户界面体验各不相同,但逻辑是一样的--从应用程序接口获取产品并呈现列表。

javascript 复制代码
const ProductsSection = () => {
  const [products, setProducts] = useState([]);

  const fetchProducts = async () => {
    try {
      const res = await fetch("https://dummyjson.com/products");
      const response = await res.json();
      setProducts(response.products);
    } catch (e) {
      console.error(e);
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <img src={product.thubmnail} alt={product.name} />
          <span>{product.name}</span>
        </li>
      ))}
    </ul>
  );
};

const ProductsCatalog = () => {
  const [products, setProducts] = useState([]);

  const fetchProducts = async () => {
    try {
      const res = await fetch("https://dummyjson.com/products");
      const response = await res.json();
      setProducts(response.products);
    } catch (e) {
      console.error(e);
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <span>Brand: {product.brand}</span>
          <span>Trade Name: {product.name}</span>
          <span>Price: {product.price}</span>
        </li>
      ))}
    </ul>
  );
};

我们可以通过 Render Props 模式轻松地重复使用这一功能:

javascript 复制代码
const ProductsList = ({ renderListItem }) => {
  const [products, setProducts] = useState([]);

  const fetchProducts = async () => {
    try {
      const res = await fetch("https://dummyjson.com/products");
      const response = await res.json();
      setProducts(response.products);
    } catch (e) {
      console.error(e);
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return <ul>{products.map((product) => renderListItem(product))}</ul>;
};

// Products Section
<ProductsList
  renderListItem={(product) => (
    <li key={product.id}>
      <img src={product.thumbnail} alt={product.title} />
      <div>{product.title}</div>
    </li>
  )}
/>

// Products Catalog
<ProductsList
  renderListItem={(product) => (
    <li key={product.id}>
      <div>Brand: {product.brand}</div>
      <div>Name: {product.title}</div>
      <div>Price: $ {product.price}</div>
    </li>
  )}
/>

使用 Render Props 模式的一些流行库包括 React RouterFormikDownshift

复合组件模式

复合组件是一种先进的 React 容器模式,它为多个组件共享状态和处理逻辑提供了一种简单而有效的方法,使它们能够协同工作。它提供了一个灵活的 API,使父组件能够与其子组件隐式地交互和共享状态。复合组件最适合需要构建声明式用户界面的 React 应用程序。一些流行的设计库(如 Ant-DesignMaterial UI 等)也采用了这种模式。

传统的 selectoptions HTML 元素的工作方式有助于我们更好地理解这种模式。选择和选项同步工作,提供一个下拉表单字段。选择元素与选项元素隐式地管理和共享其状态。因此,虽然没有明确的状态声明,但选择元素知道用户选择了什么选项。同样,我们可以根据需要使用上下文 API 在父组件和子组件之间共享和管理状态。

深入代码,让我们尝试将 Tab 组件作为一个复合组件来实现。通常,标签页有一个标签页列表,每个标签页都有一个内容部分。每次只有一个标签页处于活动状态,其内容可见。我们可以这样做

ini 复制代码
const TabsContext = createContext({});

function useTabsContext() {
  const context = useContext(TabsContext);
  if (!context) throw new Error(`Tabs components cannot be rendered outside the TabsProvider`);
  return context;
}

const TabList = ({ children }) => {
  const { onChange } = useTabsContext();

  const tabList = React.Children.map(children, (child, index) => {
    if (!React.isValidElement(child)) return null;
    return React.cloneElement(child, {
      onClick: () => onChange(index),
    });
  });

  return <div className="tab-list-container">{tabList}</div>;
};

const Tab = ({ children, onClick }) => (
  <div className="tab" onClick={onClick}>
    {children}
  </div>
);

const TabPanels = ({ children }) => {
  const { activeTab } = useTabsContext();

  const tabPanels = React.Children.map(children, (child, index) => {
    if (!React.isValidElement(child)) return null;
    return activeTab === index ? child : null;
  });

  return <div className="tab-panels">{tabPanels}</div>;
};

const Panel = ({ children }) => (
  <div className="tab-panel-container">{children}</div>
);

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

  const onChange = useCallback((tabIndex) => setActiveTab(tabIndex), []);
  const value = useMemo(() => ({ activeTab, onChange }), [activeTab, onChange]);

  return (
    <TabsContext.Provider value={value}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
};

Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanels = TabPanels;
Tabs.Panel = Panel;
export default Tabs;

现在可以用作:

javascript 复制代码
const App = () => {
  const data = [
    { title: "Tab 1", content: "Content for Tab 1" },
    { title: "Tab 1", content: "Content for Tab 1" },
  ];

  return (
    <Tabs>
      <Tabs.TabList>
        {data.map((item) => (
          <Tabs.Tab key={item.title}>{item.title}</Tabs.Tab>
        ))}
      </Tabs.TabList>
      <Tabs.TabPanels>
        {data.map((item) => (
          <Tabs.Panel key={item.title}>
            <p>{item.content}</p>
          </Tabs.Panel>
        ))}
      </Tabs.TabPanels>
    </Tabs>
  );
};

可以使用此模式的其他一些用例包括:

● 列表和列表项

● 菜单和菜单标题、菜单项、分隔线

● 表格和表格标题、表格主体、表格行、表格单元格

● 带标题和内容的手风琴

● 开关和切换

布局组件模式

在创建反应应用程序/网站时,大部分页面都会共享相同的内容。例如导航栏和页面页脚。与其在每个页面上导入每个要呈现的组件,还不如直接创建一个布局组件来得简单快捷。布局组件可以帮助我们在多个页面中轻松共享共同的部分。顾名思义,它定义了应用程序的布局。

使用可重复使用的布局是一种非常好的做法,因为它让我们只需编写一次代码,就可以在应用程序的多个部分中使用,例如:我们可以轻松地重复使用基于网格系统或 Flex Box 模型的布局。

现在,让我们来看一个布局组件的基本示例,通过它,我们可以在多个页面中共享页眉和页脚。

javascript 复制代码
const PageLayout = ({ children }) => {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
};

const HomePage = () => {
  return <PageLayout>{/* Page content goes here */}</PageLayout>;
};

⚠️:文章翻译上如有语法不准确或者内容纰漏,欢迎各位评论区指正。

【关于TalkX】

TalkX是一款基于GPT实现的IDE智能开发插件,专注于编程领域,是开发者在日常编码中提高编码效率及质量的辅助工具,TalkX常用的功能包括但不限于:解释代码、中英翻译、性能检查、安全检查、样式检查、优化并改进、提高可读性、清理代码、生成测试用例、有趣的图表生成以及语音助手托克斯等。

TalkX建立了全球加速网络,不需要考虑网络环境,响应速度快,界面效果和交互体验更流畅。并为用户提供了Open AI的密钥,不需要自备ApiKey,不需要自备账号,不需要魔法。

TalkX产品支持:JetBrains (包括 IntelliJ IDEA、PyCharm、WebStorm、Android Studio)、HBuilder、VS Code、Goland.

相关推荐
Dragon Wu5 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师5 小时前
React面试题
前端·javascript·react.js
木兮xg5 小时前
react基础篇
前端·react.js·前端框架
三思而后行,慎承诺6 小时前
Reactnative实现远程热更新的原理是什么
javascript·react native·react.js
知识分享小能手6 小时前
React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)
前端·javascript·vue.js·学习·react.js·前端框架·vue3
夏天19959 小时前
React:聊一聊状态管理
前端·javascript·react.js
LFly_ice10 小时前
学习React-11-useDeferredValue
前端·学习·react.js
LFly_ice15 小时前
学习React-10-useTransition
前端·学习·react.js
知识分享小能手15 小时前
React学习教程,从入门到精通,React 构造函数(Constructor)完整语法知识点与案例详解(16)
前端·javascript·学习·react.js·架构·前端框架·vue