React可以说是构建用户界面最流行的JavaScript库,其中一个原因是它的无偏见性。可重用的组件、出色的开发者工具和广泛的生态系统是React最受欢迎的特性之一。然而,除了其特性和社区支持之外,React还提供并实现了一些广泛使用的设计模式,以进一步简化开发过程。
在深入了解React的设计模式之前,我们应该了解它们是什么以及为什么需要它们。简而言之,设计模式是常见开发问题的可重复解决方案。它们作为基本模板,可以根据给定的要求构建任何功能,同时遵循最佳实践。我们可以使用它们来节省开发时间和减少编码工作量,因为它们作为已知问题的标准术语和经过测试的解决方案。
让我们开始吧!
这无疑是React组件中最基本和广泛使用的模式之一(可能不需要太多介绍😅)。经常需要根据某个条件来渲染或不渲染某个特定的JSX代码。这通过条件渲染来实现。例如,我们希望为未经身份验证的用户显示一个按钮,上面写着“登录”,对于已登录的用户,则显示“注销”。
通常,条件渲染可以使用&&
运算符或三元
运算符来实现。
{condition && <span>当条件为真时渲染</span>}
{condition ? <span>当条件为真时渲染</span> : <span>当条件为假时渲染</span>}
在某些情况下,我们还可以考虑使用if
、switch
或对象字面量。
React Hooks与函数组件一起被证明是一种革命性的引入。它们提供了一种简单直接的方式来访问常见的React功能,如props
、state
、context
、refs
和生命周期。我们可能满足于使用传统的hooks,但还有更多。让我们了解一下将自定义hooks引入其中的好处。想象一下,你为一个组件编写了一段逻辑,可能使用了基本的hooks,比如useEffect
和useState
。过了一段时间,相同的逻辑需要在另一个新组件中使用。虽然复制可能感觉是最快最简单的方法,但是使用自定义hooks可以更有趣(😉)。将常用逻辑提取到一个hook中可以使代码更清晰,增加可重用性,当然也更易于维护。
首先是一个常见的用例,调用不同组件中的API。想象一个组件,在从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
hook中提取出来,如下所示:
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 (...);
};
自定义hooks的其他可能用例可能包括:
● 获取窗口尺寸 ● 访问和设置本地存储 ● 在布尔状态之间切换等。
React开发人员面临的一个主要问题是Prop drilling。Prop drilling是一种情况,其中数据(props
)被传递到不同的组件,直到它到达需要该prop的组件为止。当一些数据需要传递到组件树中的一个或多个嵌套组件时,这很容易成为一个问题,因为建立了一个看似不必要的数据传递链。
这就是Provider模式的用途。Provider模式允许我们将数据(全局或可共享的)存储在一个中心位置。上下文提供者/存储可以直接将此数据传递给任何需要它的组件,而无需传递props。React的内置Context 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} />
</>
);
};
让我们看看引入Context 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中的HOCs是一种在组件中重用逻辑的高级技术。它是基于React的组合性质而创建的一种模式。它本质上包含了编程中的不要重复自己(DRY)原则。类似于JS中的高阶函数,HOCs是纯函数,它们接受一个组件作为参数并返回一个增强的组件。它符合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} />;
当处理横切关注点时,HOCs非常有用,特别是在我们希望在整个应用程序中重用组件逻辑时。一些可能的用途包括:
● 实现日志记录机制。 ● 管理授权等。
顾名思义,这种方法涉及将组件分为两个不同的类别和实现策略:
- 展示组件:这些基本上是纯粹的无状态函数组件。它们关注的是_外观如何_。它们与应用程序的任何部分都没有依赖关系,用于显示数据。- 容器组件:与展示组件不同,容器组件更负责处理事务的方式。它们作为任何副作用、有状态逻辑和展示组件本身的容器。
通过这种方法,我们实现了更好的关注点分离(因为我们不再有一个处理所有渲染和逻辑状态的复杂组件)。此外,这也提供了与展示组件更好的可重用性(因为它们没有任何依赖关系,可以轻松地在多种情况下重用)。
因此,作为开发人员,你的目标应该是创建无状态组件,即使没有立即需要重用该特定组件的情况。对于组件层次结构,最佳实践是让父组件尽可能保留更多的状态,并使子组件保持无状态。
例如,任何渲染列表的组件都可以是一个展示组件:
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 }} />;
};
Web 表单是许多应用程序中常见的需求。在 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 元素,或者使用 props 和事件回调从其他事件处理程序中重置它。
React 表单同时支持受控和非受控组件。在处理简单的 UI 和反馈时,我们可能会发现采用非受控组件更可取。对于复杂的逻辑,强烈建议使用受控组件。
根据 React 的官方文档,渲染属性是指使用函数作为值的属性来在组件之间共享代码的一种技术。与 HOC 类似,渲染属性也具有相同的目的:通过在组件之间共享有状态的逻辑来处理横切关注点。
实现渲染属性设计模式的组件采用一个返回 React 元素的函数作为属性,并调用它而不是使用自己的渲染逻辑。因此,我们可以使用函数属性来确定要渲染的内容,而不是在每个组件中硬编码逻辑。
为了更好地理解这一点,让我们举一个例子。假设我们有一个需要在应用程序的不同位置渲染的产品列表。这些位置的用户界面体验不同,但逻辑是相同的 - 从 API 获取产品并渲染列表。
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>
);
};
我们可以使用渲染属性模式轻松重用此功能:
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>;
};
// 产品部分
<ProductsList
renderListItem={(product) => (
<li key={product.id}>
<img src={product.thumbnail} alt={product.title} />
<div>{product.title}</div>
</li>
)}
/>
// 产品目录
<ProductsList
renderListItem={(product) => (
<li key={product.id}>
<div>Brand: {product.brand}</div>
<div>Name: {product.title}</div>
<div>Price: $ {product.price}</div>
</li>
)}
/>
一些使用渲染属性模式的流行库包括:React Router
、Formik
、Downshift
。
复合组件是一种高级的 React 容器模式,它提供了一种简单高效的方式,让多个组件共享状态并处理逻辑 - 协同工作。它提供了一种灵活的 API,使父组件能够隐式地与其子组件交互和共享状态。复合组件最适合需要构建声明式 UI 的 React 应用程序。这种模式也在一些流行的设计库中使用,如 Ant-Design
、Material UI
等。
传统的 select
和 options
HTML 元素的工作方式帮助我们更好地理解这一点。select 和 options 元素协同工作,提供了一个下拉表单字段。select 元素隐式地管理和共享其状态给 options 元素。因此,尽管没有显式声明状态,select 元素知道用户选择了哪个选项。类似地,在这里,我们可以使用 Context 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();```javascript
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>
);
};
其他一些可以使用此模式的用例包括:
● 列表和列表项 ● 菜单和菜单头、菜单项、分隔线。 ● 表格和表头、表体、表行、表格单元格 ● 带标题和内容的手风琴 ● 开关和切换
在创建React应用/网站时,大多数页面都会共享相同的内容。例如导航栏和页面页脚。而不是在每个页面上导入每个组件进行渲染,更容易和更快的方法是只需创建一个布局组件。布局组件帮助我们轻松地在多个页面之间共享常见的部分。正如其名称所示-它定义了应用程序的布局。
使用可重用的布局是一种很好的实践,因为它让我们只需编写一次代码,就可以在应用程序的许多部分中使用它,例如-我们可以根据网格系统或Flex Box模型轻松重用布局。
现在,让我们通过一个基本示例来考虑布局组件的使用,通过它我们可以在多个页面之间共享Header
和Footer
。
const PageLayout = ({ children }) => {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
);
};
const HomePage = () => {
return <PageLayout>{/* 页面内容放在这里 */}</PageLayout>;
};