From Render-Props to Hooks
Tags: react, javascript, webdev, hooks
Published: 2021-07-23 07:18:29

## Before you begin...
*This post is a direct followup to "[React’s Render Props in Practice](https://medium.com/myheritage-engineering/reacts-render-props-in-practice-b3775fcbf3c3)" - A post I published which describes the path from a naive component to using render props, and its benefits.
Reading it before you jump into this one will help you understand this post better.*

In almost any hooks intro talk, hooks are described as a good alternative for render-props when it comes to providing more tweaking flexibility to a component.
Yes, it makes total sense if you perceive hooks as small logic nuggets, which you can add to any component and reuse, but this all remained as something I still needed to prove for myself, given that render-props really made perfect sense to begin with and were introduced as the be-all-and-end-all solution for creating flexible components.

Back then I posted an [elaborated article](https://medium.com/myheritage-engineering/reacts-render-props-in-practice-b3775fcbf3c3) about taking a sample page navigation component and going from the naive implementation to the render-props one, to enable tweaking it much more easily. The natural next step would be to convert this component to a hooks driven component, and this is what this post is about.

So we're picking off where we have a nice basic pager component which we can use with render props to create new and diverse pager components easily.

Our starting point is the basic-pager.js component which looks like this:
```javascript
import React from "react";
 
class PagerBasic extends React.Component {
 state = {
   cursor: this.props.cursor || 0,
   pagesCount: this.props.pagesCount || 0
 };
 
 changePage = (newCursor) => {
   this.setState(
     ({ cursor }) => ({
       cursor: newCursor
     }),
     () => this.props.onPageChange(this.state.cursor)
   );
 };
 
 render() {
   const { cursor, pagesCount } = this.state;
   return this.props.children({
     cursor,
     pagesCount,
     goPrev: () => {
       this.changePage(this.state.cursor - 1);
     },
     goNext: () => {
       this.changePage(this.state.cursor + 1);
     },
     changePage: this.changePage
   });
 }
}
 
export default PagerBasic;
```
And one of its usages was this prev-next-pager component:
```javascript
function PrevNextPager(props) {
 return (
   <PagerBasic {...props}>
     {({ cursor, pagesCount, goPrev, goNext }) => {
       const prevBtnText = cursor - 1 < 0 ? "N/A" : "< Previous";
       const nextBtnText = cursor + 1 < pagesCount ? "Next >" : "N/A";
       return (
         <div>
           <span onClick={goPrev}>{prevBtnText}</span>
           {<span> | </span>}
           <span onClick={goNext}>{nextBtnText}</span>
         </div>
       );
     }}
   </PagerBasic>
 );
}

```
##Let's start
First thing first, let's take the base component and convert it from a class component to a function one:
```javascript
import React, { useState } from "react";
 
const PagerBasic = (props) => {
 const [cursor, setCursor] = useState(props.cursor || 0);
 const [pagesCount] = useState(props.pagesCount || 0);
 
 const changePage = (newCursor) => {
   setCursor(newCursor);
   props.onPageChange(cursor);
 };
 
 return props.children({
   cursor,
   pagesCount,
   goPrev: () => {
     changePage(cursor - 1);
   },
   goNext: () => {
     changePage(cursor + 1);
   },
   changePage
 });
};
 
export default PagerBasic;
```
Hmmm…. that was pretty smooth. All is still working as it used to.

Ok, so that was the first step, but we don’t want to stop here. We want to use hooks, custom hooks to be precise. We will start with the fist one, which is the **usePager** hook (still on the same file of the component):
```javascript
import React, { useState } from "react";

// The hook 
function usePager(initialCursor, initialPagesCount, pageChangeCallback) {
 const [cursor, setCursor] = useState(initialCursor);
 const [pagesCount] = useState(initialPagesCount);
 
 const changePage = (newCursor) => {
   setCursor(newCursor);
   pageChangeCallback(cursor);
 };
 
 return [cursor, pagesCount, changePage];
} 
 
// The component 
const PagerBasic = (props) => {
 const [cursor, pagesCount, changePage] = usePager(
   props.cursor || 0,
   props.pagesCount || 0,
   props.onPageChange
 );
 
 return props.children({
   cursor,
   pagesCount,
   goPrev: () => {
     changePage(cursor - 1);
   },
   goNext: () => {
     changePage(cursor + 1);
   },
   changePage
 });
};
 
export default PagerBasic;
```
Still on the same file, the hook here holds the state of the cursor position and a function to change the cursor, changePage (I know the names here can be much better, but please bear with me)

After verifying it still works, let’s extract that hook to its own file. We will call it use-pager-hook.js.
Since we need to call the props callback after the state is set, we will use **useEffect** to call it when the cursor changes (as it depends on the cursor).
We need to make sure not to call the callback on the first change of the cursor though. We do that with the help the **useRef** hook, keeping an inner state for the hook. We will also add a goNext and a goPrev functions which the hook will expose.

The hook now looks like this:
```javascript
import { useEffect, useState, useRef } from "react";
 
const usePager = ({
 cursor: initialCursor,
 pagesCount: initialPagesCount,
 onPageChange: pageChangeCallback
}) => {
 const [cursor, setCursor] = useState(initialCursor);
 const [pagesCount] = useState(initialPagesCount);
 
 const firstUpdate = useRef(true);
 
 useEffect(() => {
   if (firstUpdate.current) {
     firstUpdate.current = false;
   } else {
     pageChangeCallback(cursor);
   }
 }, [cursor, pageChangeCallback]);
 
 const changePage = (newCursor) => {
   setCursor(newCursor);
 };
 
 const goPrev = () => {
   changePage(cursor - 1);
 };
 
 const goNext = () => {
   changePage(cursor + 1);
 };
 
 return [cursor, pagesCount, goPrev, goNext, changePage];
};
 
export default usePager;

```
And it’s usage looks something like this:
```javascript
import React from "react";
import usePager from "./use-pager-hook";
 
const PagerBasic = (props) => {
 const [cursor, pagesCount, changePage] = usePager(
   props.cursor || 0,
   props.pagesCount || 0,
   props.onPageChange
 );
 
 return props.children({
   cursor,
   pagesCount,
   goPrev: () => {
     changePage(cursor - 1);
   },
   goNext: () => {
     changePage(cursor + 1);
   },
   changePage
 });
};
 
export default PagerBasic;
```

Now the question is, do we still need the PagerBasic, or can we use this hook in the component which PagerBasic wraps, such as the PrevNextPager component.
Let’s try and use our newly created custom hook instead:
```javascript
function PrevNextPager(props) {
 const [cursor, pagesCount, goPrev, goNext] = usePager(props);
 const [prevBtnText, setPrevBtnText] = useState("");
 const [nextBtnText, setNextBtnText] = useState("");
 
 useEffect(() => {
   setPrevBtnText(cursor - 1 < 0 ? "N/A" : "< Previous");
   setNextBtnText(cursor + 1 < pagesCount ? "Next >" : "N/A");
 }, [cursor, pagesCount]);
 
 return (
   <div>
     <span onClick={goPrev}>{prevBtnText}</span>
     {<span> | </span>}
     <span onClick={goNext}>{nextBtnText}</span>
   </div>
 );
}
```
And the other, more complex component “QuickPager”? There it is:
```javascript
function QuickPager(props) {
 const [cursor, pagesCount, goPrev, goNext, changePage] = usePager(props);
 const [prevBtnText, setPrevBtnText] = useState("");
 const [nextBtnText, setNextBtnText] = useState("");
 
 const buffer = new Array(props.pagesBuffer).fill(0); // useMemo?
 
 useEffect(() => {
   setPrevBtnText(cursor - 1 < 0 ? "N/A" : "< Previous");
   setNextBtnText(cursor + 1 < pagesCount ? "Next >" : "N/A");
 }, [cursor, pagesCount]);
 
 return (
   <div>
     <span onClick={goPrev}>{prevBtnText}</span>
     {buffer.map((item, index) => {
       const pageCursor = cursor + index;
       const className = pageCursor === cursor ? "selected" : "normal";
       return pageCursor >= 0 && pageCursor < pagesCount ? (
         <span
           key={`page-${pageCursor}`}
           onClick={() => changePage(pageCursor)}
           className={className}
         >
           {` [${pageCursor}] `}
         </span>
       ) : null;
     })}
     <span onClick={goNext}>{nextBtnText}</span>
   </div>
 );
}
```

Wait now, this can also go into a custom hook:
```javascript
useEffect(() => {
   setPrevBtnText(cursor - 1 < 0 ? "N/A" : "< Previous");
   setNextBtnText(cursor + 1 < pagesCount ? "Next >" : "N/A");
 }, [cursor, pagesCount]);
```
So this is how our **useNavigationBtns** hook looks like (again, please ignore the horrible naming):
```javascript
import { useState, useEffect } from "react";
 
const useNavigationBtns = ({ cursor, pagesCount } = {}) => {
 const [prevBtnText, setPrevBtnText] = useState("");
 const [nextBtnText, setNextBtnText] = useState("");
 
 useEffect(() => {
   setPrevBtnText(cursor - 1 < 0 ? "N/A" : "< Previous");
   setNextBtnText(cursor + 1 < pagesCount ? "Next >" : "N/A");
 }, [cursor, pagesCount]);
 
 return [prevBtnText, nextBtnText];
};
 
export default useNavigationBtns;
```
And we use it like this which makes our components even more cleaner:
```javascript
function PrevNextPager(props) {
 const [cursor, pagesCount, goPrev, goNext] = usePager(props);
 const [prevBtnText, nextBtnText] = useNavigationBtns({ cursor, pagesCount });
 
 return (
   <div>
     <span onClick={goPrev}>{prevBtnText}</span>
     {<span> | </span>}
     <span onClick={goNext}>{nextBtnText}</span>
   </div>
 );
}
```

Super cool, I must admit :)

## Conclusion
So yeah, this is not just theory talking but actually a methodology which can make our components much cleaner, with less code to write and better separation of concerns between the different logic parts which comprise a whole component.

Hope you found this helpful. Be sure to leave a comment if you have any questions or something to bring up!

cheers

*Hey! If you liked what you've just read be sure to also visit me on twitter* :) **<a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>** :beers: