写点什么

为什么 React Hooks 优于 HOCs(译)

用户头像
西贝
关注
发布于: 2020 年 11 月 03 日
为什么 React Hooks 优于 HOCs(译)

原文

https://www.robinwieruch.de/react-hooks-higher-order-components



In a modern React world, everyone uses function components with React Hooks. However, the concept of higher-order components (HOCs) is still applicable in a modern React world, because they can be used for class components and function components. Therefore they are the perfect bridge for using reusable abstractions among legacy and modern React components.



在当今的 React 世界,每个人都在使用具有 React Hooks 的函数组件。然而,高阶组件(HOCs)的概念也同样适用,因为它们可以在 Class 组件和函数组件中被使用。因此在可复用的抽象性上,它们是沟通传统和当今React组件的完美桥梁



I am still an advocate for higher-order components these days because their composable nature of enhancing components fascinates me. However, there are problems with HOCs which shouldn't be denied and which are entirely solved by React Hooks. This is why I want to point out these problems, so that developers can make an informed decision whether they want to use an HOC over an Hook for certain scenarios or whether they just want to go all-in with React Hooks after all.



现如今我仍旧是高阶组件的拥护者,因为它们的高度可组合特性吸引着我。然而,HOCs也存在一些不可被忽略的问题,并且这些问题在 React Hooks 中已经被解决。这也是为什么我想要指出这些问题,因为只有指出问题,开发者们才能在面对问题时做出合理的决定,比如在特殊的场景,他们更多的是想用HOC而不是使用Hook,或者他们想全部用React Hooks来解决所有问题



HOCS VS HOOKS: PROP CONFUSION



HOCS VS HOOKS:prop 的困惑



Let's take the following higher-order component (HOC) which is used for a conditional rendering. If there is an error, it renders an error message. If there is no error, it renders the given component:



我们以下面这个用于条件渲染的高阶组件(HOC)为例。如果产生错误,它会渲染一个错误信息;如果没有错误,它会渲染指定的组件:



import * as React from 'react';
const withError = (Component) => (props) => {
if (props.error) {
return <div>Something went wrong ...</div>;
}
return <Component {...props} />;
};
export default withError;



Note how the HOC passes all the props to the given component if there is no error. Everything should be working fine this way, however, there may be too many props passed to the next component which isn't necessarily concerned about all of them.



请注意,如果没有产生错误,HOC是如何将props传递给组件的。一切都按照指定的方式正常进行着,但是,这样的话,可能会存在多余的props被传递给下一个组件,而这个组件并不需要关心这些属性是什么



For example, it could be that the next component doesn't care at all about the error, thus it would be a better practice to remove the error with a rest operator from the props before forwarding the props to the next component:



根据上面的示例,下一个组件对于这个错误信息根本不关心,因此在向下一个组件传递props之前,用rest 操作符将错误信息去掉是更好的实现方案



import * as React from 'react';
const withError = (Component) => ({ error, ...rest }) => {
if (error) {
return <div>Something went wrong ...</div>;
}
return <Component {...rest} />;
};
export default withError;



This version should work as well, at least if the given component doesn't need the error prop. However, these both versions of a HOC already show the surfacing problem of prop confusion when using HOCs. Most often props are just passed through HOCs by using the spread operator and only partly used in the HOC itself. Often it isn't clear from the start whether the given component needs all the props provided to the HOC (first version) or is just fine with only a part of the props (second version).



调整后的版本应该可以更好的使用,至少这个指定的组件不会再获取到错误属性。但是这两个版本的HOC在使用过程中都表现出了prop混乱的问题。大多数情况下,props只是通过扩展运算符传递给HOCs,并且仅有部分属性会作用在HOC上。通常在一开始的时候并不清楚指定的组件是需要所有的props来支持这个HOC(第一版)还是仅仅一部分props就可以(第二版)。



That's the first caveat of using a HOC; which gets quickly unpredictable when using multiple HOCs which are composed onto each other, because then one has not only to consider what props are needed for the given component, but also what props are needed for the other HOCs in the composition. For example, let's say we have another HOC for rendering a conditional loading indicator:



这是用HOC时的第一处警示:当多个HOC彼此组合使用时,情况会很快变得不可控,因为,这样做的的话,不仅仅需要考虑当前指定的组件需要哪些props属性,也需要考虑组合使用的其它HOCs需要哪些props属性。假如我们有另一个用于渲染带条件的loading效果的HOC



import * as React from 'react';
const withLoading = (Component) => ({ isLoading, ...rest }) => {
if (isLoading) {
return <div>Loading ...</div>;
}
return <Component {...rest} />;
};
export default withLoading;



Both HOCs, withError and withLoading are composed on a component now. Once this component is used, it may look like the following:



现在,两个HOC,withErrorwithLoading 在一个组件上是稳定的。一旦这个组件被调用,它可能会向下面这样:



const DataTableWithFeedback = compose(
withError,
withLoading,
)(DataTable);
const App = () => {
...
return (
<DataTableWithFeedback
columns={columns}
data={data}
error={error}
isLoading={isLoading}
/>
);
};



Without knowing the implementation details of the HOCs, would you know which props are consumed by the HOCs and which are dedicated the the underlying component? It's not clear which props are really passed through to the actual DataTable component and which props are consumed by HOCs on the way.



在不知道HOC实现细节的基础上,你知道哪些props被用于HOC,哪些用于实现底层组件?在这个过程中,并不清楚哪些props被实际的作用于DataTable组件,哪些props被HOC所使用



Let's take this example one step further, by introducing another HOC for data fetching where we don't show the implementation details:



基于上面的例子,我们做进一步的扩展,介绍另一个用于实现数据请求的HOC,但我们并不展示这个组件的实现细节



const DataTableWithFeedback = compose(
withFetch,
withError,
withLoading,
)(DataTable);
const App = () => {
...
const url = 'https://api.mydomain/mydata';
return (
<DataTableWithFeedback
url={url}
columns={columns}
/>
);
};



Suddenly we don't need dataisLoading, and error anymore, because all this information is generated in the new withFetch HOC by using the url. What's interesting though is that isLoading and error, while generated inside the withFetch HOC, will already be consumed on the way by withLoading and withError. On the other hand, the generated (here fetched) data from withFetch will be passed as prop to the underlying DataTable component.



猛然发现,我们不在需要 dataloadingerror 这些属性,因为,所有的这些信息通过 url在这个新的HOC withFetch 中被生成,尤其是 isLoadingerror,当在 withFetch 中被生成时,它们已经在withLoadingwithError 中被调用。另外,在 withFetch 中生成的数据 data 会作为props 被下沉到DataTable组件中



App withFetch withError withLoading DataTable
data-> data-> data-> data
url-> error-> error
isLoading-> isLoading-> isLoading



In addition to all of this hidden magic, see how order matters too: withFetch needs to be the outer HOC while withLoading and withError follow without any particular order here which gives lots of room for bugs.



除了这些隐藏魔法之外,也需要关注一下顺序问题:withFetch 作为一个外部的HOC,而 withLoadingwithError 则没有任何调用顺序,这样就存在了bugs隐患



In conclusion, all these props coming in and out from HOCs travel somehow through a blackbox which we need to examine with a closer look to really understand which props are produced on the way, which props are consumed on the way, and which props get passed through. Without looking into the HOCs, we don't know much about what happens between these layers.



总而言之,所有这些从HOCs中进出的props都会以某种方式经过一个黑盒,我们需要仔细的研究这个盒子才能真正的理解,哪些props是在这个过程中被产生的,哪些props被调用了,以及哪些props被下沉。没有真正研究过这个HOC,我们就不知道在这些层级之间到底发生了什么



Finally, in comparison, let's see how React Hooks solve this issue with one -- easy to understand from a usage perspective -- code snippet:



最后,做一个比较,我们通过一个代码片段来看React Hooks 是如何解决这个问题的,从使用者的角度来看,更容易理解这段代码



const App = () => {
const url = 'https://api.mydomain/mydata';
const { data, isLoading, error } = useFetch(url);
if (error) {
return <div>Something went wrong ...</div>;
}
if (isLoading) {
return <div>Loading ...</div>;
}
return (
<DataTable
columns={columns}
data={data}
/>
);
};



When using React Hooks, everything is laid out for us: We see all the props (here url) that are going into our "blackbox" (here useFetch) and all the props that are coming out from it (here dataisLoadingerror). Even though we don't know the implementation details of useFetch, we clearly see which input goes in and which output comes out. And even though useFetch can be treated as a blackbox like withFetch and the other HOCs, we see the whole API contract with this React Hook in just one plain line of code.



当使用React Hooks时,所有的一切都呈现在我们面前:我们可以看到所有进入黑盒(useFetch)的props(url)和从它产出的props(dataisLoadingerror)。虽然我们不知道 useFetch 的实现细节,但我们可以清晰地看到哪些是输入,哪些是输出。而且,useFetch 虽然像withFetch 和其它HOCs一样是一个黑盒,但我们可以发现仅需要一行代码,就可以将整个API和React Hooks 建立关联



This wasn't as clear with HOCs before, because we didn't clearly see which props were needed (input) and which props were produced (output). In addition, there are not other HTML layers in between, because we just use the conditional rendering in the parent (or in the child) component.



以前在使用HOCs时,这些并不清晰,因为我们并不清楚哪些props是被需要的(输入),哪些props是被生成的(输出)。另外,在彼此之间也没有其它的 HTML 层级,因为我们仅仅是在父组件(或子组件)中进行条件渲染




HOCS VS HOOKS: NAME CONFLICTS/COLLISION



HOCS VS HOOKS: 命名冲突



If you give a component a prop with the same name two times, the latter will override the former:



假如你给一个组件的同一个prop赋值两次,那么后面的会覆盖前面的值



<Headline text="Hello World" text="Hello React" />



When using a plain component like in the previous example, this issue gets quite obvious and we are less likely to override props accidentally (and only on purpose if we need to). However, with HOCs this gets messy again when two HOCs pass props with the same name.



当调用上面这个文本组件时,这个问题会十分的明显,而且我们很少会故意的复写props(除非在必要情况下)。然而,在HOC中,当两个HOC调用具有相同名称的props时,就会出现问题



The easiest illustration for this problem is by composing two identical HOCs on top of a component:



对于这个问题,最常见的场景时在一个组件上挂载两个完全一样的HOC



const UserWithData = compose(
withFetch,
withFetch,
withError,
withLoading,
)(User);
const App = () => {
...
const userId = '1';
return (
<UserWithData
url={`https://api.mydomain/user/${userId}`}
url={`https://api.mydomain/user/${userId}/profile`}
/>
);
};



This is a very common scenario; often components need to fetch from multiple API endpoints.



这是一个很常见的场景;通常组件需要从多个API端发起请求



As we have learned before, the withFetch HOC expects an url prop for the data fetching. Now we want to use this HOC two times and thus we are not able anymore fulfil both HOCs contract. In contrast, both HOCs will just operate on the latter URL which will lead to a problem. A solution (and yes, there is more than one solution) to this problem would be changing our withFetch HOC to something more powerful in order to perform not a single but multiple requests:



根据我们之前学到的,withFetch 需要一个 url 属性用于发起数据请求。现在我们需要调用两次这个HOC,并且我们不会再遵守HOC之间的协议。在协议中,两个HOC都只会使用后面的URL,从而产生错误。解决问题的一个方案(当然,不止一个方案)是调整我们的withFetch,使其具有更强的功能,不单单只支持一个请求,而是可以同时支持多个请求



const UserWithData = compose(
withFetch,
withError,
withLoading,
)(User);
const App = () => {
...
const userId = '1';
return (
<UserWithData
urls={[
`https://api.mydomain/user/${userId}`,
`https://api.mydomain/user/${userId}/profile`,
]}
/>
);
};



This solution seems plausible, but let's let this sink in for a moment: The withFetch HOC, previously just concerned about one data fetching -- which based on this one data fetching sets states for isLoading and error -- suddenly becomes a monster of complexity. There are many questions to answer here:



这个解决方案貌似是可行的,但是让我们再思考一下这个问题:withFetch 之前仅仅支持一个数据请求,这个请求被用于设置isLoadingerror 的状态,现在突然这个数据请求变成了一个复杂类型。从而产生了许多问题需要被解决:



  • Does the loading indicator still show up even though one of the requests finished earlier?

  • Does the whole component render as an error if only one request fails?

  • What happens if one request depends on another request?

  • ...



  • 假如其中一个请求已经提前完成,那loading标识是否仍然保持?

  • 如果其中一个请求发生错误,那整个组件作为一个错误进行渲染?

  • 如果一个请求依赖于另一个请求怎么办?

  • ……



Despite of this making the HOC already a super complex (yet powerful) HOC -- where my personal gut would tell me it's too powerful -- we introduced another problem internally. Not only did we have the problem of passing a duplicated prop (here url, which we solved with urls) to the HOC, but also the HOC will output a duplicate prop (here data) and pass it to the underlying component.



尽管这样做会使这个HOC变的更复杂(更强大),至少我个人认为它很强大,但是我们还是引入了另一个问题。我们这样做,不仅仅存在将复杂的prop(url,此处我们用 urls 代替)传递给HOC的问题,也存在HOC将生成复杂的prop(data),并将它下沉给之后的组件的问题



That's why, in this case the User component has to receive a merged data props -- the information from both data fetches -- or has to receive an array of data -- whereas the first entry is set accordingly to the first URL and the second entry accordingly to the second URL. In addition, when both requests don't fulfil in parallel, one data entry can be empty while the other one is already there ...



这也就是为什么,在这个示例中User组件不得不接收一个来自于两个数据请求合并后的data,,或者接收一个数据数组,其中第一个实体数据来自第一个URL,第二个实体数据来自第二个URL,当两个请求并不是并行完成时,一个数据实体可能是空的,而另一个数据已经存在



Okay. I don't want to go any further fixing this here. There are solutions to this, but as I mentioned earlier, it would lead to making the withFetch HOC more complicated than it should be and the situation of how to use the merged data or data array in the underlying component not much better from a developer's experience perspective.



OK,我不想要在这个基础上做进一步的修改。对于这个问题,确实有解决方案,但是就像我之前提过的,它会导致withFetch 比它应该的样子更复杂,而且在底层组件中如何使用合并后的data 或者 data 数组,对于开发者而言,也并没有使情况变的更好



Let's see how React Hooks solve this for us with one -- easy to understand from a usage perspective -- code snippet again:



接下来我们再用一段代码片段来看看React Hooks是如何解决这个问题的,从使用者的角度可能更容易理解



const App = () => {
const userId = '1';
const {
data: userData,
isLoading: userIsLoading,
error: userError
} = useFetch(`https://api.mydomain/user/${userId}`);
const {
data: userProfileData,
isLoading: userProfileIsLoading,
error: userProfileError
} = useFetch(`https://api.mydomain/user/${userId}/profile`);
if (userError || userProfileError) {
return <div>Something went wrong ...</div>;
}
if (userIsLoading) {
return <div>User is loading ...</div>;
}
const userProfile = userProfileIsLoading
? <div>User profile is loading ...</div>
: <UserProfile userProfile={userProfileData} />;
return (
<User
user={userData}>
userProfile={userProfile}
/>
);
};



Do you see the flexibility we gain here? We only return early with an loading indicator if the user is still loading, however, if the user is already there and only the user profile is pending, we are only partially rendering a loading indicator where the data is missing (here also due to the power of component composition). We could do the same for the error, however, because we gained all this power over how to deal with the outcome of the requests, we can render the same error message for both errors. If we later decide we want to deal with both errors differently, we can do this in this one component and not in our abstraction (whether it's HOC or Hook).



发现了吗?我们获得了灵活性。如果用户当前仍是loading态,我们会用一个loading标识提前返回,然而,如果用户已经进入,并且只有用户的配置项是挂起状态,我们只要在数据缺失的位置进行局部loading渲染(这也是组合组件的优势)。对于错误,我们也可以做出相同的处理,因为我们已经得到了这些请求结果的所有内容,我们可以对两个结果渲染相同的错误信息,假设以后我们想要分别处理两个错误,我们也可以在这个组件内完成,而不是在我们的抽象(HOC或者Hook)中完成



After all, and that's why we come to this conclusion in the first place, we avoided the naming collision by renaming the variables which comes as output from the React Hooks within the object destructuring. When using HOCs, we need to be aware of HOCs maybe using the same names for props internally. It's often obvious when using the same HOC twice, but what happens if you are using two different HOCs which -- just by accident -- use the same naming for a prop? They would override each others data and leave you baffled why your receiving component doesn't get the correct props.



最后,这就是为什么我们优先得出的结论,我们通过对React Hooks解构得出的变量进行重命名的方式解决命名冲突的问题,当使用HOC时,我们需要提前知道HOC内部可能会用到相同名称的props。这种在同一个HOC中调用两次的情况是很明显的,但是假如你是在两个不同的HOC中(特殊情况下)用了相同名称的prop,会发生什么?它们会彼此覆盖,并且你会困惑,为什么你的接收组件没有获取到正确的props

HOCS VS HOOKS: DEPENDENCIES

HOCS VS HOOKS: 依赖性



HOCs are powerful, perhaps too powerful? HOCs can receive arguments two ways: When they receive props from the parent component (as we have seen before) and when they enhance a component. Let's elaborate the latter by example.



HOCs 是强大的,也许是太强大了?HOC可以通过两种途径接收参数:从父组件接收props(正如之前我们看到的)和增加一个组件,我们通过下面的例子对后者进行说明



Take our withLoading and withError HOCs from before but this time more powerful:



和之前一样,调用withLoadingwithError 两个HOC,但这次它们会更强大



const withLoading = ({ loadingText }) => (Component) => ({ isLoading, ...rest }) => {
if (isLoading) {
return <div>{loadingText ? loadingText : 'Loading ...'}</div>;
}
return <Component {...rest} />;
};
const withError = ({ errorText }) => (Component) => ({ error, ...rest }) => {
if (error) {
return <div>{errorText ? errorText : 'Something went wrong ...'}</div>;
}
return <Component {...rest} />;
};



With these extra arguments -- here passed through a higher-order function surrounding the HOC -- we gain additional power to provide arguments when creating the enhanced component with our HOCs:



利用这些额外的参数(这里通过一个高阶函数封装一个HOC),当我们用HOC来创建一个更大的组件时,我们可以获得提供参数的能力



const DataTableWithFeedback = compose(
withError({ errorText: 'The data did not load' }),
withLoading({ loadingText: 'The data is loading ...' }),
)(DataTable);
const App = () => {
...
return (
<DataTableWithFeedback
columns={columns}
data={data}
error={error}
isLoading={isLoading}
/>
);
};



This contributes an (1) positive and (2) negative effect to the Prop Confusion problem from before, because now we have (2) more than one place from where the HOC receives props (which doesn't make things easier to understand), but then again (1) we can avoid the implicit prop passing from the parent component (where we don't know whether this prop is consumed by the HOC or the underlying component) and try to pass props from the very beginning when enhancing the component instead.



针对之前Prop混淆的问题,提供了正反两种效果,因为现在我们从HOC接收的props不止来自一处位置(这样并没有使事情变得容易理解),但是相对的,我们可以避免来自父组件的隠式prop(我们不知道prop是HOC使用还是底层组件使用),并且尝试在一开始强化组件的时候就传递props



However, in the end, these arguments (here the objects with errorText and loadingText) passed when enhancing the component are static. We are not able to interpolate them with any props from the parent component here, because we are creating the composed component outside of any component. For instance, in the data fetching example we wouldn't be able to introduce a flexible user ID:



然而,在最后,这些在强化组件时传递的参数(此处指errorTextloadingText)是静态的。我们不会用任何来自父组件的props对它们进行赋值,因为我们需要在这些组件外面创建一个组合组件。比如,在数据请求示例中,我们不会引入一个灵活的用户ID



const UserWithData = compose(
withFetch('https://api.mydomain/user/1'),
withFetch('https://api.mydomain/user/1/profile'),
)(User);
const App = () => {
...
return (
<UserWithData
columns={columns}
/>
);
};



Even though there are ways to overcome this, it doesn't make this whole props passing any more easier to understand:



虽然有许多方法可以实现这个,但是它并没有使整个props的传输变的更容易理解



const UserWithData = compose(
withFetch(props => `https://api.mydomain/user/${props.userId}`),
withFetch(props => `https://api.mydomain/user/${props.userId}/profile`),
)(User);
const App = () => {
...
const userId = '1';
return (
<UserWithData
userId={userId}
columns={columns}
/>
);
};



Making this scenario even more complex by adding another challenge: What happens if the second request depends on the first request? For instance, the first request returns an user by ID and the second request returns a user's profile based on the profileId which we only get with the first request:



为了使这个场景变得更复杂,我们引入另一个挑战:假设第二个请求依赖第一个请求,怎么实现呢?比如,第一个请求使用ID返回一个用户,第二个请求在profileId的基础上返回用户的配置,而profileId 来自第一个请求



const UserProfileWithData = compose(
withFetch(props => `https://api.mydomain/users/${props.userId}`),
withFetch(props => `https://api.mydomain/profile/${props.profileId}`),
)(UserProfile);
const App = () => {
...
const userId = '1';
return (
<UserProfileWithData
columns={columns}
userId={userId}
/>
);
};



We introduced two HOCs which are tightly coupled here. In another solution, we may have created one powerful HOC to solve this for us. However, this shows us that it's difficult to create HOCs which depend on each other.



我们引入了两个紧耦合的HOC。在另一个方案中,我们为解决这个问题已经创建了一个强化的HOC。然而,这个例子向我们展示了,依赖另一个组件创建HOC是很困难的



In contrast, let's see how this mess is solved by React Hooks again:



相对的,我们再来看一下React Hooks 是如何解决这个情况的



const App = () => {
const userId = '1';
const {
data: userData,
isLoading: userIsLoading,
error: userError
} = useFetch(`https://api.mydomain/user/${userId}`);
const profileId = userData?.profileId;
const {
data: userProfileData,
isLoading: userProfileIsLoading,
error: userProfileError
} = useFetch(`https://api.mydomain/user/${profileId}/profile`);
if (userError || userProfileError) {
return <div>Something went wrong ...</div>;
}
if (userIsLoading || userProfileIsLoading) {
return <div>Is loading ...</div>;
}
return (
<User
user={userData}>
userProfile={userProfileData}
/>
);
};



Because React Hooks can be used directly in a function component, they can build up onto each other and it's straightforward to pass data from one hook to another hook if they depend on each other. There is also no real blackbox again, because we can clearly see which information needs to be passed to these custom hooks and which information comes out from them. When using React Hooks that depend on each other, the dependencies are more explicit compared to using HOCs.



因为 React Hooks 是可以直接被用在一个函数组件中,所以它们是可以建立在彼此之上的,并且如果它们彼此依赖,从一个hook向另一个hook传递数据也是很方便的。并且不再有真正的黑盒,因为我们可以清楚的看到哪些信息需要被传递给这些自定义的hooks,哪些信息是来自于它们。当使用这种彼此依赖的React Hooks时,依赖关系比用HOC更清晰


In the aftermath, I am still a big fan of HOCs for shielding away complexity from components (e.g. conditional rendering, protected routes). But as these last scenarios have shown, they are not always the best solution. Hence my recommendation would be using React Hooks instead.



最后,我仍旧是HOC的fans,因为它将组件的复杂性进行了封装(比如,条件渲染,路由守卫等)。但是正如最后的这些场景所展示的,它们并不一定是最好的解决方案。因此,我的建议还是用 React Hooks



发布于: 2020 年 11 月 03 日阅读数: 76
用户头像

西贝

关注

还未添加个人签名 2019.02.15 加入

还未添加个人简介

评论

发布
暂无评论
为什么 React Hooks 优于 HOCs(译)