hoc-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHOC Pattern
HOC模式
Within our application, we often want to use the same logic in multiple components. This logic can include applying a certain styling to components, requiring authorization, or adding a global state.
One way of being able to reuse the same logic in multiple components, is by using the higher order component pattern. This pattern allows us to reuse component logic throughout our application.
在我们的应用中,我们经常希望在多个组件中使用相同的逻辑。这些逻辑可以包括为组件应用特定样式、权限校验或添加全局状态。
在多个组件中复用相同逻辑的一种方式是使用higher order component(HOC)模式。该模式允许我们在整个应用中复用组件逻辑。
When to Use
使用场景
- Use this when the same uncustomized behavior needs to be applied to many components
- This is helpful when a component should work standalone without the added custom logic
- 当多个组件需要应用相同的无定制化行为时使用
- 当组件需要在不添加自定义逻辑的情况下独立运行时,这种模式很有帮助
Instructions
实现指南
- Create a function that takes a component and returns a new component with enhanced behavior
- Avoid naming collisions by renaming or merging props in the HOC
- Prefer React Hooks over HOCs for most new code to avoid wrapper hell and deep nesting
- Compose multiple HOCs carefully and be aware that the order of composition matters
- 创建一个接收组件并返回具有增强行为的新组件的函数
- 通过重命名或合并props避免HOC中的命名冲突
- 对于大多数新代码,优先使用React Hooks而非HOC,以避免“包装地狱”和深层嵌套
- 谨慎组合多个HOC,并注意组合顺序会影响结果
Details
详细说明
A Higher Order Component (HOC) is a component that receives another component. The HOC contains certain logic that we want to apply to the component that we pass as a parameter. After applying that logic, the HOC returns the element with the additional logic.
Say that we always wanted to add a certain styling to multiple components in our application. Instead of creating a object locally each time, we can simply create a HOC that adds the objects to the component that we pass to it:
stylestylejs
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)We just created a StyledButton and StyledText component, which are the modified versions of the Button and Text component. They now both contain the style that got added in the HOC!
withStylesLet's improve the user experience a little bit. When we're fetching the data, we want to show a screen to the user. Instead of adding data to the component directly, we can use a Higher Order Component that adds this logic for us.
"Loading..."DogImagesLet's create a HOC called . A HOC should receive a component, and return that component. In this case, the HOC should receive the element which should display until the data is fetched.
withLoaderwithLoaderLoading…js
function withLoader(Element) {
return (props) => <Element />;
}However, we don't just want to return the element it received. Instead, we want this element to contain logic that tells us whether the data is still loading or not.
To make the HOC very reusable, we won't hardcode the Dog API url in that component. Instead, we can pass the URL as an argument to the HOC, so this loader can be used on any component that needs a loading indicator while fetching data from a different API endpoint.
withLoaderwithLoaderjs
function withLoader(Element, url) {
return (props) => {};
}A HOC returns an element, a functional component in this case, to which we want to add the logic that allows us to display a text with as the data is still being fetched. Once the data has been fetched, the component should pass the fetched data as a prop.
props => {}Loading…We just created a HOC that can receive any component and url.
- In the hook, the
useEffectHOC fetches the data from the API endpoint that we pass as the value ofwithLoader. While the data hasn't returned yet, we return the element containing theurltext.Loading... - Once the data has been fetched, we set equal to the data that has been fetched. Since
datais no longerdata, we can display the element that we passed to the HOC!null
So, how can we add this behavior to our application? In , we no longer want to just export the plain component. Instead, we want to export the "wrapped" HOC around the component.
DogImages.jsDogImageswithLoadingDogImagesjs
export default withLoader(
DogImages,
"https://dog.ceo/api/breed/labrador/images/random/6"
);The Higher Order Component pattern allows us to provide the same logic to multiple components, while keeping all the logic in one single place. The HOC doesn't care about the component or url it receives: as long as it's a valid component and a valid API endpoint, it'll simply pass the data from that API endpoint to the component that we pass.
withLoaderHigher Order Component(HOC)是一种接收另一个组件的组件。HOC包含我们想要应用于传入组件的特定逻辑,应用逻辑后,HOC会返回带有额外逻辑的元素。
假设我们希望为应用中的多个组件添加特定样式。与其每次在本地创建对象,我们可以创建一个HOC,将对象添加到传入的组件中:
stylestylejs
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)我们刚刚创建了StyledButton和StyledText组件,它们是Button和Text组件的修改版本。现在它们都包含了 HOC中添加的样式!
withStyles让我们稍微提升用户体验。当我们获取数据时,我们希望向用户显示“Loading...”界面。与其直接在组件中添加数据逻辑,我们可以使用一个HOC来为我们添加该逻辑。
DogImages让我们创建一个名为的HOC。HOC应该接收一个组件并返回该组件。在这个例子中, HOC应该接收一个元素,该元素需要在数据获取完成前显示“Loading…”。
withLoaderwithLoaderjs
function withLoader(Element) {
return (props) => <Element />;
}不过,我们不只是想返回接收到的元素。相反,我们希望这个元素包含判断数据是否仍在加载的逻辑。
为了让 HOC具有高度可复用性,我们不会在该组件中硬编码Dog API的URL。相反,我们可以将URL作为参数传递给 HOC,这样这个加载器就可以用于任何需要在从不同API端点获取数据时显示加载指示器的组件。
withLoaderwithLoaderjs
function withLoader(Element, url) {
return (props) => {};
}HOC返回一个元素,在这个例子中是一个函数式组件,我们需要向其添加逻辑,以便在数据仍在获取时显示“Loading…”文本。一旦数据获取完成,组件应该将获取到的数据作为props传递。
props => {}我们刚刚创建了一个可以接收任何组件和URL的HOC。
- 在钩子中,
useEffectHOC从我们传入的withLoader对应的API端点获取数据。在数据返回之前,我们返回包含“Loading...”文本的元素。url - 一旦数据获取完成,我们将设置为获取到的数据。由于
data不再是data,我们就可以显示传递给HOC的元素了!null
那么,我们如何将这种行为添加到应用中呢?在中,我们不再只想导出普通的组件。相反,我们想要导出被 HOC包装后的组件。
DogImages.jsDogImageswithLoadingDogImagesjs
export default withLoader(
DogImages,
"https://dog.ceo/api/breed/labrador/images/random/6"
);高阶组件模式允许我们为多个组件提供相同的逻辑,同时将所有逻辑集中在一个地方。 HOC不关心它接收的组件或URL:只要是有效的组件和API端点,它就会将该API端点的数据传递给我们传入的组件。
withLoaderComposing
组合
We can also compose multiple Higher Order Components. Let's say that we also want to add functionality that shows a text box when the user hovers over the list.
Hovering!DogImagesWe need to create a HOC that provides a prop to the element that we pass. Based on that prop, we can conditionally render the text box based on whether the user is hovering over the list.
hoveringDogImagesWe can now wrap the HOC around the HOC.
withHoverwithLoaderThe element now contains all props that we passed from both and . We can now conditionally render the text box, based on whether the value of the prop is or .
DogImageswithHoverwithLoaderHovering!hoveringtruefalseA well-known library used for composing HOCs is recompose. Since HOCs can largely be replaced by React Hooks, the recompose library is no longer maintained.
我们还可以组合多个高阶组件。假设我们还想添加一个功能,当用户悬停在列表上时显示一个“Hovering!”文本框。
DogImages我们需要创建一个HOC,为传入的元素提供一个 prop。基于这个prop,我们可以根据用户是否悬停在列表上来有条件地渲染文本框。
hoveringDogImages现在我们可以将 HOC包装在 HOC之外。
withHoverwithLoaderDogImageswithHoverwithLoaderhoveringtruefalse一个用于组合HOC的知名库是recompose。由于HOC在很大程度上可以被React Hooks替代,recompose库已不再维护。
Hooks
Hooks
In some cases, we can replace the HOC pattern with React Hooks.
Let's replace the HOC with a hook. Instead of having a higher order component, we export a hook that adds a and event listener to the element. We cannot pass the element anymore like we did with the HOC. Instead, we'll return a from the hook that should get the and events.
withHoveruseHovermouseOvermouseLeaverefmouseOvermouseLeaveThe hook adds an event listener to the component, and sets the value to or , depending on whether the user is currently hovering over the element. Both the and values need to be returned from the hook: to add a ref to the component that should receive the and events, and in order to be able to conditionally render the text box.
useEffecthoveringtruefalserefhoveringrefmouseOvermouseLeavehoveringHovering!Instead of wrapping the component with the component, we can simply use the hook within the component directly.
DogImageswithHoveruseHoverGenerally speaking, React Hooks don't replace the HOC pattern.
"In most cases, Hooks will be sufficient and can help reduce nesting in your tree." - React Docs
As the React docs tell us, using Hooks can reduce the depth of the component tree. Using the HOC pattern, it's easy to end up with a deeply nested component tree.
js
<withAuth>
<withLayout>
<withLogging>
<Component />
</withLogging>
</withLayout>
</withAuth>By adding a Hook to the component directly, we no longer have to wrap components.
Note (React 18+): Modern React strongly prefers Hooks or other composition patterns to reuse logic with less nesting. Excessive layering of HOCs leads to "wrapper hell" with deeply nested wrappers, making debugging difficult and cluttering React DevTools. Libraries like Recompose are no longer maintained because Hooks can largely replace their functionality. For example, instead of aHOC, you can create awithHoverhook that returns whether an element is hovered and a ref to attach. This avoids an extra component layer and meshes well with React's upcoming features — the React Compiler can better analyze component code when logic isn't hidden in HOCs.useHover
Using Higher Order Components makes it possible to provide the same logic to many components, while keeping that logic all in one single place. Hooks allow us to add custom behavior from within the component, which could potentially increase the risk of introducing bugs compared to the HOC pattern if multiple components rely on this behavior.
Best use-cases for a HOC:
- The same, uncustomized behavior needs to be used by many components throughout the application.
- The component can work standalone, without the added custom logic.
Best use-cases for Hooks:
- The behavior has to be customized for each component that uses it.
- The behavior is not spread throughout the application, only one or a few components use the behavior.
- The behavior adds many properties to the component
在某些情况下,我们可以用React Hooks替代HOC模式。
让我们用钩子替换 HOC。我们不再使用高阶组件,而是导出一个钩子,为元素添加和事件监听器。我们不能再像使用HOC那样传递元素,而是从钩子中返回一个,该应该绑定和事件。
useHoverwithHovermouseOvermouseLeaverefrefmouseOvermouseLeaveuseEffecthoveringtruefalserefhoveringrefmouseOvermouseLeavehovering与其用组件包装组件,我们可以直接在组件内部使用钩子。
withHoverDogImagesuseHover一般来说,React Hooks并不会完全替代HOC模式。
“在大多数情况下,Hooks已经足够,并且可以帮助减少组件树的嵌套。” - React Docs
正如React文档所述,使用Hooks可以减少组件树的深度。使用HOC模式很容易导致组件树深度嵌套。
js
<withAuth>
<withLayout>
<withLogging>
<Component />
</withLogging>
</withLayout>
</withAuth>通过直接在组件中添加Hook,我们不再需要包装组件。
注意(React 18+): 现代React强烈推荐使用Hooks或其他组合模式来复用逻辑,同时减少嵌套。过多的HOC分层会导致“包装地狱”,组件包装层级过深,使调试变得困难,并使React DevTools变得混乱。像Recompose这样的库已不再维护,因为Hooks可以在很大程度上替代它们的功能。例如,你可以创建一个钩子来返回元素是否被悬停以及一个要绑定的ref,而不是使用useHoverHOC。这避免了额外的组件层,并且与React即将推出的功能更兼容——当逻辑不隐藏在HOC中时,React编译器可以更好地分析组件代码。withHover
使用高阶组件可以为多个组件提供相同的逻辑,同时将所有逻辑集中在一个地方。Hooks允许我们在组件内部添加自定义行为,与HOC模式相比,如果多个组件依赖此行为,这可能会增加引入bug的风险。
HOC的最佳使用场景:
- 整个应用中的多个组件需要使用相同的、无定制化的行为。
- 组件可以在不添加自定义逻辑的情况下独立运行。
Hooks的最佳使用场景:
- 每个使用该行为的组件都需要定制化该行为。
- 该行为并非在整个应用中广泛使用,只有一个或少数组件使用。
- 该行为为组件添加了多个属性。
Case Study
案例研究
Some libraries that relied on the HOC pattern added Hooks support after the release. A good example of this is Apollo Client.
One way to use Apollo Client is through the higher order component. With the HOC, we can make data from the client available to components that are wrapped by the higher order component! Although we can still use the HOC currently, there are some downsides to using it.
graphql()graphql()graphql()When a component needs access to multiple resolvers, we need to compose multiple higher order components in order to do so. Composing multiple HOCs can make it difficult to understand how the data is passed to your components. The order of the HOCs can matter in some cases, which can easily lead to bugs when refactoring the code.
graphql()After the release of Hooks, Apollo added Hooks support to the Apollo Client library. Instead of using the higher order component, developers can now directly access the data through the hooks that the library provides.
graphql()By using the hook, we reduced the amount of code that was needed in order to provide the data to the component.
useMutationBesides a reduction in boilerplate, it's also much easier to use the data of multiple resolvers in a component. Instead of having to compose multiple higher order components, we can simply write multiple hooks in the component. Knowing how data gets passed to the component is much easier this way, and improves developer experience when refactoring components, or breaking them down into smaller pieces.
一些依赖HOC模式的库在Hooks发布后添加了Hooks支持。Apollo Client就是一个很好的例子。
使用Apollo Client的一种方式是通过高阶组件。通过 HOC,我们可以让客户端的数据对被高阶组件包装的组件可用!尽管我们现在仍然可以使用 HOC,但它存在一些缺点。
graphql()graphql()graphql()当一个组件需要访问多个解析器时,我们需要组合多个高阶组件才能实现。组合多个HOC会使理解数据如何传递到组件变得困难。在某些情况下,HOC的顺序很重要,这在重构代码时很容易导致bug。
graphql()Hooks发布后,Apollo为Apollo Client库添加了Hooks支持。开发者现在可以直接通过库提供的钩子访问数据,而不是使用高阶组件。
graphql()通过使用钩子,我们减少了向组件提供数据所需的代码量。
useMutation除了减少样板代码外,在组件中使用多个解析器的数据也变得更加容易。我们不再需要组合多个高阶组件,只需在组件中编写多个钩子即可。这样更容易了解数据如何传递到组件,并且在重构组件或将其拆分为更小的部分时提升了开发者体验。
Pros
优点
Using the Higher Order Component pattern allows us to keep logic that we want to re-use all in one place. This reduces the risk of accidentally spreading bugs throughout the application by duplicating code over and over, potentially introducing new bugs each time. By keeping the logic all in one place, we can keep our code DRY and easily enforce separation of concerns.
使用高阶组件模式可以将我们想要复用的逻辑集中在一个地方。这减少了通过重复代码在整个应用中意外传播bug的风险,每次重复代码都可能引入新的bug。通过将逻辑集中在一个地方,我们可以保持代码DRY(Don't Repeat Yourself),并轻松实现关注点分离。
Cons
缺点
The name of the prop that a HOC can pass to an element, can cause a naming collision.
js
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)In this case, the HOC adds a prop called to the element that we pass to it. However, the component already had a prop called , which will be overwritten! Make sure that the HOC can handle accidental name collision, by either renaming the prop or merging the props.
withStylesstyleButtonstylejs
function withStyles(Component) {
return props => {
const style = {
padding: '0.2rem',
margin: '1rem',
...props.style
}
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)When using multiple composed HOCs that all pass props to the element that's wrapped within them, it can be difficult to figure out which HOC is responsible for which prop. This can hinder debugging and scaling an application easily.
HOC传递给元素的prop名称可能会导致命名冲突。
js
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)在这个例子中, HOC向传入的元素添加了一个名为的prop。然而,组件已经有一个名为的prop,它会被覆盖!请确保HOC能够处理意外的命名冲突,可以通过重命名prop或合并props来解决。
withStylesstyleButtonstylejs
function withStyles(Component) {
return props => {
const style = {
padding: '0.2rem',
margin: '1rem',
...props.style
}
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)当使用多个组合的HOC,且它们都向被包装的元素传递props时,很难确定哪个HOC负责哪个prop。这会阻碍应用的调试和扩展。