Consider the implementation of a data request to the API using a new friend React Hooks and good old friends Render Prop and HOC (Higher Order Component). Find out whether the new friend is better than the old two.
Life does not stand still, React is changing for the better. In February 2019, React Hooks appeared in React 16.8.0. Now in the functional components, you can work with the local state and perform side effects. Nobody believed that this was possible, but everyone always wanted it. If you are not aware of the details yet, for details here .
React Hooks make it possible to finally abandon such patterns as HOC and Render Prop. Because during the use of them a number of complaints have accumulated:
RProp | HOC | |
---|---|---|
1. There are many wrapper components that are difficult to understand in React DevTools and in the code. | (βοΈ΅β) | (βοΈ΅β) |
2. Difficult to type (Flow, TypeScript). | (βοΈ΅β) | |
3. It is not obvious from which HOC which component the props receives, which complicates the process of debugging and understanding how the component works. | (βοΈ΅β) | |
4. Render Prop most often does not add layout, although it is used inside JSX. | (βοΈ΅β) | |
5. Props key collision. When transmitting props from parents, the same keys can be overwritten with values ββfrom the HOC. | (βοΈ΅β) | |
6. It is difficult to read git diff, since all indents in JSX are shifted when JSX is wrapped in Render Prop. | (βοΈ΅β) | |
7. If several HOC, then you can make a mistake with the sequence of the composition. The correct order is not always obvious, since the logic is hidden inside the HOC. For example, when we first check whether the user is authorized, and only then we request personal data. | (βοΈ΅β) |
Not to be unfounded, let's look at an example of how React Hooks is better (and maybe worse) Render Prop. We will consider Render Prop, not HOC, since they are very similar in implementation and HOC has more drawbacks. Let's try to write a utility that processes the data request to the API. I am sure that many have written this in their lives hundreds of times, so what can we see if it can be even better and simpler.
For this we will use the popular axios library. In the simplest scenario, the following states need to be processed:
1. Simple script
We write the default state and the function (reducer), which changes the state depending on the result of the request: success / error.
For reference. Reducer came to us from functional programming, and for most JS developers from Redux. This is a function that takes the previous state and action (action) and returns the next state.
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; } }
We reuse this function in two approaches.
Render prop
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); }
React hooks
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]; };
By url from the used component we get the data - axios.get (). We process success and error, changing state through dispatch (action). We return state in component. And don't forget to cancel the request if the url is changed or if the component is removed from the DOM. Everything is simple, but you can write in different ways. Highlight the pros and cons of the two approaches:
Hooks | RProp | |
---|---|---|
1. Less code. | (ββΏβ) | |
2. The call to the side effect (data request to the API) is easier to read, since it is written linearly, not spread over the component life cycles. | (ββΏβ) | |
3. Cancel request written immediately after the call request. All in one place. | (ββΏβ) | |
4. A simple code that describes the tracking parameters for calling side effects. | (ββΏβ) | |
5. Obviously, the life cycle of the component will execute our code. | (ββΏβ) |
React Hooks allow you to write less code, and this is an indisputable fact. So, the effectiveness of you as a developer is growing. But you have to master the new paradigm.
When there are names of life cycles of a component, everything is very clear. First, we receive the data after the component appears on the screen (componentDidMount), then we re-receive if the props.url has changed and do not forget to cancel the previous request (componentDidUpdate) before that, if the component is removed from the DOM, then cancel the request (componentWillUnmount) .
But now we are causing the side effect right in the render, but we were taught that it was wrong. Although stop, not quite in the render. And inside the useEffect function, which will perform something asynchronously after each render, or rather, the commit and draw of a new DOM.
But we do not need after each render, but only on the first render, and if the url is changed, we specify the second argument in useEffect.
Understanding how React Hooks work requires an awareness of new things. For example, the difference between the phases: commit and render. In the render phase, React calculates which changes to apply in the DOM by comparing with the result of the previous render. And in the commit phase, React applies these changes to the DOM. It is during the commit phase that the methods are called: componentDidMount and componentDidUpdate. But what is written in useEffect will be called asynchronously after the commit and, thus, will not block the DOM drawing if you accidentally decide something to synchronize a lot in the side effect.
Conclusion - use useEffect. Write less and safer.
And another great feature: useEffect can clean up after the previous effect and after removing a component from the DOM. Thanks to Rx who inspired the React team to take this approach.
Using our utility with React Hooks is also much more convenient.
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" />; };
The React Hooks option again looks more compact and obvious.
Cons Render Prop:
1) it is not clear whether the layout is added or only the logic
2) if it is necessary to process the state from Render Prop in the local state or the life cycles of the child component, you will have to create a new component
Add a new functionality - data acquisition with new parameters according to user action. I wanted, for example, a button that gets the avatar of your favorite developer.
2) Update user action data
Add a button that sends a request with a new username. The simplest solution is to store the username in the local state of the component and transfer the new username from the state, not props as it is now. But then we have to copy-paste wherever we need similar functionality. So we will carry out this functionality in our utility.
We will use this:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Let's write the implementation. Only the changes from the original version are written below.
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 }; ... } }
Render prop
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); } }
React hooks
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]; };
If you carefully looked at the code, you noticed:
Pay attention with Render Prop, we had to use getDerivedStateFromProps to update the local state in case of changing props.url. And with React Hooks, no new abstractions, you can immediately cause a state update in the render - hooray, comrades, finally!
The only complication with React Hooks was that the update function had to be memorized so that it would not change between component updates. When, like in Render Prop, the update function is a class method.
3) Poll API at equal time or polling
Let's add another popular functionality. Sometimes you need to constantly poll the API. Not only did your favorite developer change the avatar, and you do not know. Add parameter spacing.
Using:
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 ); ...
Implementation:
function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } }
Render prop
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(); } ...
React hooks
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]); ... }
There is a new prop - pollInterval. Upon completion of the previous request via setTimeout, we increment the requestId. With hooks, we have another useEffect in which we call setTimeout. And our old useEffect, which sends the request, began to follow one more variable - requestId, which tells us that setTimeout has worked, and it's time to send the request for a new avatar.
In Render Prop I had to write:
React Hooks allow us to write shortly and clearly what we are used to describe in more detail and is not always clear.
4) What's next?
We can continue to expand the functionality of our utility: to accept different configurations of query parameters, data caching, response and error conversion, forced data update with the same parameters β routine operations in any large web application. On our project, we have long carried this into a separate (attention!) Component. Yes, because it was Render Prop. But with the release of Hooks, we rewrote the function (useAxiosRequest) and even found some bugs in the old implementation. You can see and try here .
Source: https://habr.com/ru/post/453866/
All Articles