在深入了解 React 设计模式的细节之前,我们应该先了解它们是什么以及为什么需要它们。简单地说,设计模式是针对常见开发问题的可重复解决方案。
【参考文献】
文章:《Understanding Design Patterns in React》
上述译文仅供参考,具体内容请查看上面链接,解释权归原作者所有。
【关于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
运算符来实现的。
{condition && <span>Rendered when `truthy`</span>}
{condition ? <span>Rendered when `truthy`</span> : <span>Rendered when `falsy`</span>}
在某些情况下,我们还可以考虑使用if
、switch
或对象字面形式。
自定义钩子
事实证明,React 挂钩是与功能组件相结合的革命性引入。它们提供了一种简单而直接的方式来访问 props
, state
, context
, refs
和生命周期等常见的 React 功能。我们可能满足于使用传统的钩子,但还有更多。让我们来了解一下引入自定义钩子的好处。想想你为一个组件编写的逻辑,你可能使用了基本的钩子,如 useEffect
和 useState
。一段时间后,需要在另一个新组件中使用相同的逻辑。虽然复制可能是最快速、最简单的方法,但自定义钩子来实现同样的效果会更有趣。在钩子中提取通常需要的逻辑可以使代码更简洁,提高可重用性,当然还有可维护性。
从一个常见的用例开始,在不同的组件中调用 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-redux
、flux
、MobX
等。
要理解这一点,可以举一个例子,在应用程序中实现明/暗主题就是一个常见的场景。如果不使用 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)
举例来说,一个简单的组件可以渲染用户列表并处理加载、出错和无可用数据等各种状态。
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 非常有用。
一些可能的用法如下:
● 实施日志记录机制。
● 管理授权等。
呈现与容器组件模式
顾名思义,这种方法就是将组件分为两种不同的类别和实施策略:
呈现组件: 这些组件本质上是纯粹的无状态功能组件。它们关注的是事物的外观。它们与应用程序的任何部分都没有任何依赖关系,用于显示数据。
容器组件: 与展示组件不同,容器组件更多的是负责工作方式。它们充当任何副作用、有状态逻辑和呈现组件本身的容器。
通过这种方法,我们可以更好地分离关注点(因为我们不会只有一个复杂的组件来处理所有的呈现和逻辑状态)。此外,这种方法还能更好地重用呈现组件(因为它们不存在任何依赖关系,因此可以轻松地在多个场景中重用)。
因此,作为开发人员,即使没有必须重用特定组件的直接场景,也应该以创建无状态组件为目标。对于组件的层次结构,最好的做法是让父组件尽可能多地保留状态,并创建无状态的子组件。
举例来说,任何渲染列表的组件都可以是呈现组件:
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 元素,或者使用道具和事件回调从其他事件处理程序中重置值。
React 表单支持受控和不受控组件。在某些用例中,我们可能需要处理简单的用户界面和反馈,这时我们可能会发现最好采用不受控组件。对于复杂的逻辑,我们强烈建议使用受控组件。
渲染道具模式
根据 React 的官方文档,渲染道具(Render Prop)指的是一种使用道具(prop)在组件间共享代码的技术,道具的值是函数。与 HOC 类似,Render Props 也具有相同的目的:通过在组件之间共享有状态逻辑来处理交叉问题。
实施渲染道具设计模式的组件会将返回 React 元素的函数作为道具,然后调用它,而不是使用其渲染逻辑。因此,我们可以使用函数 prop 来决定呈现什么,而不是在每个组件内部硬编码逻辑。
为了更好地理解这一点,让我们举一个例子。假设我们有一个产品列表,需要在应用程序的不同位置呈现。这些位置的用户界面体验各不相同,但逻辑是一样的–从应用程序接口获取产品并呈现列表。
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``````Router
、Formik
、Downshift
。
复合组件模式
复合组件是一种先进的 React 容器模式,它为多个组件共享状态和处理逻辑提供了一种简单而有效的方法,使它们能够协同工作。它提供了一个灵活的 API,使父组件能够与其子组件隐式地交互和共享状态。复合组件最适合需要构建声明式用户界面的 React 应用程序。一些流行的设计库(如 Ant-Design
、Material UI
等)也采用了这种模式。
传统的 select
和 options
HTML 元素的工作方式有助于我们更好地理解这种模式。选择和选项同步工作,提供一个下拉表单字段。选择元素与选项元素隐式地管理和共享其状态。因此,虽然没有明确的状态声明,但选择元素知道用户选择了什么选项。同样,我们可以根据需要使用上下文 API 在父组件和子组件之间共享和管理状态。
深入代码,让我们尝试将 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>
);
};
可以使用此模式的其他一些用例包括:
● 列表和列表项
● 菜单和菜单标题、菜单项、分隔线
● 表格和表格标题、表格主体、表格行、表格单元格
● 带标题和内容的手风琴
● 开关和切换
布局组件模式
在创建反应应用程序/网站时,大部分页面都会共享相同的内容。例如导航栏和页面页脚。与其在每个页面上导入每个要呈现的组件,还不如直接创建一个布局组件来得简单快捷。布局组件可以帮助我们在多个页面中轻松共享共同的部分。顾名思义,它定义了应用程序的布局。
使用可重复使用的布局是一种非常好的做法,因为它让我们只需编写一次代码,就可以在应用程序的多个部分中使用,例如:我们可以轻松地重复使用基于网格系统或 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.