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

0
在深入了解 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 代码。这可以通过条件呈现来实现。例如,我们希望向未认证用户显示 "登录 "按钮,向已登录用户显示 "注销 "按钮。

1

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

{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。一段时间后,需要在另一个新组件中使用相同的逻辑。虽然复制可能是最快速、最简单的方法,但自定义钩子来实现同样的效果会更有趣。在钩子中提取通常需要的逻辑可以使代码更简洁,提高可重用性,当然还有可维护性。

2

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

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钩子中轻松提取这一功能:

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等。

3

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

const App = ({ theme }) => {
  return (
    <>
      <Header theme={theme} />
      <Main theme={theme} />
      <Footer theme={theme} />
    </>
  );
};

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

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

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)

4

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

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 中实现这一点,我们可以这样做

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 非常有用。

一些可能的用法如下:

● 实施日志记录机制。

● 管理授权等。

呈现与容器组件模式

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

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

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

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

5

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

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

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

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

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

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

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

受控与非受控组件模式

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

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

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访问输入。这种方法可以在需要时从字段中提取值。现在让我们看看这个表单的受控版本会是什么样子:

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 元素,或者使用道具和事件回调从其他事件处理程序中重置值。

6

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

渲染道具模式

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

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

7

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

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 模式轻松地重复使用这一功能:

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等)也采用了这种模式。

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

7

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

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;

现在可以用作:

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>
  );
};

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

● 列表和列表项

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

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

● 带标题和内容的手风琴

● 开关和切换

布局组件模式

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

8

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

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

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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值