具有React Hooks,HOC或Render Prop的API请求


让我们考虑使用新朋友React Hooks和好朋友Render Prop和HOC(高阶组件)对API的数据请求的实现。 找出新朋友是否真的比旧朋友好。


生活不会停滞不前,React变得越来越好。 在2019年2月,React Hooks出现在React 16.8.0中。 现在,在功能组件中,您可以使用局部状态并执行副作用。 没有人相信这是可能的,但是每个人都一直希望。 如果您不了解最新信息,请单击此处获取详细信息。


React Hooks使得最终放弃诸如HOC和Render Prop之类的模式成为可能。 因为在使用过程中,针对它们积累了许多主张:


RProp特设
1.许多包装组件在React DevTools和代码中很难理解。(◕︵◕)(◕︵◕)
2.很难键入(Flow,TypeScript)。(◕︵◕)
3.哪个组件支持哪个HOC并不明显,这使调试过程和理解组件如何工作变得复杂。(◕︵◕)
4.尽管JSX内部使用Render Prop,但通常不添加布局。(◕︵◕)
5.按键碰撞道具。 从父母那里传递道具时,相同的键可以被HOC中的值覆盖。(◕︵◕)
6.读取git diff非常困难,因为将JSX包装在Render Prop中时,JSX中的所有缩进都会移动。(◕︵◕)
7.如果有多个HOC,则您可能会误认为合成顺序。 正确的顺序并不总是很明显,因为逻辑隐藏在HOC内部。 例如,当我们首先检查用户是否被授权,然后才请求个人数据。(◕︵◕)

为了没有根据,让我们来看一个示例,该示例说明React Hooks如何更好(或更糟)Render Prop。 我们将考虑Render Prop,而不是HOC,因为在实现上它们非常相似,并且HOC具有更多缺点。 让我们尝试编写一个实用程序来处理对API的数据请求。 我敢肯定,许多人已经在生活中写下了数百遍,好吧,让我们看看它是否有可能变得越来越好。


为此,我们将使用流行的axios库。 在最简单的情况下,您需要处理以下状态:


  • 数据采集​​过程(isFetching)
  • 数据成功接收(responseData)
  • 接收数据时出错(错误)
  • 取消请求,如果在执行过程中请求参数已更改,并且您需要发送新的
  • 如果此组件不再在DOM中,则取消请求

1.简单场景


我们将编写默认状态和一个根据请求结果更改状态的函数(缩减器):成功/错误。


什么是Reducer?

供参考。 Reducer来自函数编程,对于Redux的大多数JS开发人员来说都是如此。 此函数采用上一个状态和操作并返回下一个状态。


const defaultState = { responseData: null, isFetching: true, error: null }; function reducer1(state, action) { switch (action.type) { case "fetched": return { ...state, isFetching: false, responseData: action.payload }; case "error": return { ...state, isFetching: false, error: action.payload }; default: return state; } } 

我们通过两种方法重用此功能。


渲染道具


 class RenderProp1 extends React.Component { state = defaultState; axiosSource = null; tryToCancel() { if (this.axiosSource) { this.axiosSource.cancel(); } } dispatch(action) { this.setState(prevState => reducer(prevState, action)); } fetch = () => { this.tryToCancel(); this.axiosSource = axios.CancelToken.source(); axios .get(this.props.url, { cancelToken: this.axiosSource.token }) .then(response => { this.dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { this.dispatch({ type: "error", payload: error }); }); }; componentDidMount() { this.fetch(); } componentDidUpdate(prevProps) { if (prevProps.url !== this.props.url) { this.fetch(); } } componentWillUnmount() { this.tryToCancel(); } render() { return this.props.children(this.state); } 

反应钩


 const useRequest1 = url => { const [state, dispatch] = React.useReducer(reducer, defaultState); React.useEffect(() => { const source = axios.CancelToken.source(); axios .get(url, { cancelToken: source.token }) .then(response => { dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { dispatch({ type: "error", payload: error }); }); return source.cancel; }, [url]); return [state]; }; 

通过使用的组件的url,我们获得数据-axios.get()。 我们处理成功和错误,通过调度(操作)更改状态。 将状态返回到组件。 并且,如果URL发生更改或从DOM中删除了组件,请不要忘记取消请求。 这很简单,但是您可以用不同的方式编写。 我们重点介绍两种方法的优缺点:


钩子RProp
1.更少的代码。(◑‿◐)
2.调用副作用(在API中请求数据)更容易阅读,因为它是线性编写的,不会散布在组件的整个生命周期中。(◑‿◐)
3.请求取消后立即写入请求取消。 全部集中在一处。(◑‿◐)
4.简单的代码,描述了用于触发副作用的跟踪参数。(◑‿◐)
5.显然,我们的代码将在哪个组件生命周期中执行。(◑‿◐)

React Hooks允许您编写更少的代码,这是不争的事实。 这意味着您作为开发人员的效率正在提高。 但是您必须掌握一个新的范例。


当有组件生命周期的名称时,一切都非常清楚。 首先,我们在组件出现在屏幕上之后获取数据(componentDidMount),然后,如果props.url发生了更改,我们将再次获取数据;在此之前,我们不要忘记取消先前的请求(componentDidUpdate),如果已将组件从DOM中删除,则取消请求(componentWillUnmount) 。


但是现在我们直接在渲染中引起了副作用,我们被告知这是不可能的。 尽管停止,但实际上并没有在渲染中。 在useEffect函数内部,该函数将在每次渲染后异步执行某些操作,或者更确切地说,提交并渲染新的DOM。


但是,我们不需要在每次渲染之后,仅在第一个渲染时使用,并且在更改url的情况下(我们将其表示为useEffect的第二个参数)。


新范式

了解React Hooks的工作方式需要对新事物有所了解。 例如,阶段之间的差异:提交和渲染。 在渲染阶段,React通过与之前渲染的结果进行比较,计算出哪些更改要应用到DOM中。 在提交阶段,React将这些更改应用于DOM。 方法在提交阶段被调用:componentDidMount和componentDidUpdate。 但是useEffect中编写的内容将在提交后异步调用,因此,如果您突然无意中决定要同步许多副作用,则不会阻塞DOM呈现。


结论-使用useEffect。 写得更少,更安全。


还有一个很棒的功能:useEffect可以在上一个效果之后以及从DOM中删除该组件之后进行清理。 感谢Rx启发了React团队采用这种方法。


将我们的实用程序与React Hooks一起使用也更加方便。


 const AvatarRenderProp1 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`}> {state => { if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }} </RenderProp> ); 

 const AvatarWithHook1 = ({ username }) => { const [state] = useRequest(`https://api.github.com/users/${username}`); if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }; 

React Hooks选项再次显得更加紧凑和明显。


缺点渲染道具:


1)不清楚是添加布局还是仅添加逻辑
2)如果您需要在本地状态或子组件的生命周期中处理来自Render Prop的状态,则必须创建一个新组件


添加新功能-通过用户操作接收带有新参数的数据。 例如,我想要一个按钮来获取您最喜欢的开发人员的头像。


2)更新用户动作数据


添加一个按钮,使用新的用户名发送请求。 最简单的解决方案是将用户名存储在组件的本地状态中,然后从状态(而不是现在的props)中转移新的用户名。 但是,在需要类似功能的任何地方,我们都会进行复制粘贴。 因此,我们将此功能放入了我们的实用程序。


我们将像这样使用它:


 const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... }; 

让我们编写一个实现。 下面只写了与原始版本相比的更改。


 function reducer2(state, action) { switch (action.type) { ... case "update url": return { ...state, isFetching: true, url: action.payload, defaultUrl: action.payload }; case "update url manually": return { ...state, isFetching: true, url: action.payload, defaultUrl: state.defaultUrl }; ... } } 

渲染道具


 class RenderProp2 extends React.Component { state = { responseData: null, url: this.props.url, defaultUrl: this.props.url, isFetching: true, error: null }; static getDerivedStateFromProps(props, state) { if (state.defaultUrl !== props.url) { return reducer(state, { type: "update url", payload: props.url }); } return null; } ... componentDidUpdate(prevProps, prevState) { if (prevState.url !== this.state.url) { this.fetch(); } } ... update = url => { this.dispatch({ type: "update url manually", payload: url }); }; render() { return this.props.children(this.state, this.update); } } 

反应钩


 const useRequest2 = url => { const [state, dispatch] = React.useReducer(reducer, { url, defaultUrl: url, responseData: null, isFetching: true, error: null }); if (url !== state.defaultUrl) { dispatch({ type: "update url", payload: url }); } React.useEffect(() => { …(fetch data); }, [state.url]); const update = React.useCallback( url => { dispatch({ type: "update url manually", payload: url }); }, [dispatch] ); return [state, update]; }; 

如果您仔细查看该代码,则会发现:


  • url开始存储在我们的实用程序中;
  • defaultUrl似乎标识该URL是通过props更新的。 我们需要监视props.url的更改,否则将不会发送新请求;
  • 添加了更新功能,单击按钮,我们将返回到组件以发送新请求。

请注意,对于渲染道具,我们必须使用getDerivedStateFromProps来更新本地状态,以防props.url发生更改。 使用React Hooks没有新的抽象,您可以立即在渲染器中调用状态更新-欢呼,同志们,终于!


使用React Hooks的唯一麻烦是记住更新功能,以便它不会在组件更新之间更改。 如Render Prop中所述,更新功能是类方法时。


3)以相同的时间间隔轮询API或轮询


让我们添加另一个受欢迎的功能。 有时您需要不断查询API。 您永远不会知道您最喜欢的开发人员是否更改了个人资料图片,而且您也不知道。 添加interval参数。


用法:


 const AvatarRenderProp3 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}> ... 

 const AvatarWithHook3 = ({ username }) => { const [state, update] = useRequest( `https://api.github.com/users/${username}`, 1000 ); ... 

实现方式:


 function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } } 

渲染道具


 class RenderProp3 extends React.Component { state = { ... requestId: 1, } ... timeoutId = null; ... tryToClearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); } } poll = () => { this.tryToClearTimeout(); this.timeoutId = setTimeout(() => { this.dispatch({ type: 'poll' }); }, this.props.pollInterval); }; ... componentDidUpdate(prevProps, prevState) { ... if (this.props.pollInterval) { if ( prevState.isFetching !== this.state.isFetching && !this.state.isFetching ) { this.poll(); } if (prevState.requestId !== this.state.requestId) { this.fetch(); } } } componentWillUnmount() { ... this.tryToClearTimeout(); } ... 

反应钩


 const useRequest3 = (url, pollInterval) => { const [state, dispatch] = React.useReducer(reducer, { ... requestId: 1, }); React.useEffect(() => { …(fetch data) }, [state.url, state.requestId]); React.useEffect(() => { if (!pollInterval || state.isFetching) return; const timeoutId = setTimeout(() => { dispatch({ type: "poll" }); }, pollInterval); return () => { clearTimeout(timeoutId); }; }, [pollInterval, state.isFetching]); ... } 

出现了一个新的道具-pollInterval。 通过setTimeout完成上一个请求后,我们增加requestId。 对于钩子,我们还有另一个useEffect,在其中调用setTimeout。 我们发送请求的旧useEffect开始监视另一个变量-requestId,该变量告诉我们setTimeout有效,现在该向新的化身发送请求了。


在Render Prop中,我必须编写:


  1. 比较先前和新的requestId和isFetching值
  2. 在两个地方清除timeoutId
  3. 将timeoutId属性添加到类中

React Hooks可以让您简洁明了地写出我们以前所描述的内容,但并不总是很清楚。


4)接下来呢?
我们可以继续扩展实用程序的功能:接受不同的查询参数配置,缓存数据,转换响应和错误,使用相同参数强制更新数据-在任何大型Web应用程序中的常规操作。 在我们的项目中,很久以前我们已经将其带入一个单独的(注意!)组件。 是的,因为它是一个渲染道具。 但是随着Hooks的发布,我们重写了函数(useAxiosRequest),甚至在旧的实现中发现了一些错误。 您可以在这里查看并尝试。

Source: https://habr.com/ru/post/zh-CN453866/


All Articles