Custom Hooks in React: Hands-on
Custom hooks are great to add flexibility to your code and reuse logic, with the help of the alreay existing react hooks in a creative way.
Through this blog post we will have a look at how to create custom hooks, and just how flexible we can be with them. To demonstrate this flexibility, we will use a pretty ridiculous use case: a Witcher needing a potion refill to boost his stats.
Creating a local project
Let us create a local react (typescript) project:
yarn create react-app custom-hooks --template typescript
We’ll remove the pre-populated stuff from our App.tsx file, so that only the following shell remains:
...function App() {
return (
<div>
</div>
);
}...
Cool, now let us get to defining our logic a little.
First, let us create a few interfaces to lay down the foundation for what we are about to do. In your App.tsx file, add the following interfaces right after your import statements at the top:
interface Stats {
hp: number;
attack: number;
defence: number;
}export interface Potion {
name: string;
stats: Stats;
}export interface Character {
name: string;
stats: Stats;
potions: Array<Potion>;
}...
As you can infer, we have created interfaces to define a ‘Character’, which is the entity having some stats and potions (which can be used to boost these stats).
Let us now proceed to acquire our witcher, Geralt of Rivia. Add the following code below your interfaces:
...const Geralt: Character = {
name: "Geralt of Rivia",
stats: {
hp: 3200,
attack: 650,
defence: 880,
},
potions: [
{
name: "Full Moon",
stats: {
hp: 400,
attack: 0,
defence: 0,
},
},
{
name: "Thunderbolt",
stats: {
hp: 0,
attack: 400,
defence: 0,
},
},
],
};...
We created the stats interface earlier, since stats for a potion and those for a character look quite similar as you can see above. We have used the hp, attack and defence keys again to indicate the boosts that the potion offers.
Now let us get to our App functional component, so we can use our webpage to display the witcher’s statistics.
import React, { useState } from 'react';...export default function App() { // we track the Geralt object with useState now
// don't forget to import useState first!!!
const [witcher, setWitcher] = useState(Geralt); return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<div>
<h3>Name: {witcher.name}</h3>
<div>
<h4>Health: {witcher.stats.hp}</h4>
</div>
<div>
<h4>Attack: {witcher.stats.attack}</h4>
</div>
<div>
<h4>Defence: {witcher.stats.defence}</h4>
</div>
<div>
{witcher.potions.map((potion) => (
<div key={potion.name}>
{potion.name}
</div>
))}
</div>
</div>
</div>
);
}
What we have done above, is simply added the Geralt object we created earlier to local state with the help of the useState hook provided by React.
Besides this, we have just listed down Geralt’s stats on our page. We have used inline styling to keep things as concise as possible for the scope of this blog of course.
Try seeing these changes in your browser and it should look something like this:
Now onto the fun part, creating the custom hook.
Creating your own hooks
A custom is nothing but a javascript function, only that it will be using the already existing hooks provided by react. The other special thing about this function is that it needs to be prepended with “use”, like all other react hooks are.
Since we are creating a hook to help our witcher use potions to enhance his stats, we shall call our hook: usePotion (unsurprisingly). Our hook will only allow one potion to be used per invocation, hence usePotion and not “usePotions”.
Great, let’s next create a directory to keep our custom hook in. In your src folder, create a directory called ‘hooks’. In this directory, create a file called usePotion.tsx to store our custom hook in.
Now let us first create the function for our hook:
import React from "react";export default function usePotion() {};
We will take in a Character and a Potion as arguments for this hook.
We will then update the Character’s current stats with the available stats from the Potion.
If you remember, earlier we had added the following interfaces in our App.tsx file:
...export interface Potion {
name: string;
stats: Stats;
}export interface Character {
name: string;
stats: Stats;
potions: Array<Potion>;
}...
As we exported our interfaces, we are now able to use them in our usePotion.tsx file:
import React from "react";
import { Character, Potion } from "../App";export default function usePotion(
character: Character,
potion: Potion | null
) {};
Using Existing React hooks
The code we have so far in our usePotion.tsx file looks like:
import React from "react";
import { Character, Potion } from "../App";export default function usePotion(
character: Character,
potion: Potion | null
) {};
Let us now import useState, to track the current potion and character statistics. We will do this as follows:
import React, { useState } from "react";
import { Character, Potion } from "../App";export default function usePotion(
character: Character,
potion: Potion | null
) {
const [
currentPotion,
setCurrentPotion
] = useState(potion);
const [
currentCharacterStats,
setCurrentCharacterStats
] = useState(character.stats);};
Let us now add a useEffect as well, which will reflect changes in state according to the currentPotion that is used.
import React, { useState, useEffect } from "react";
import { Character, Potion } from "../App";export default function usePotion(
character: Character,
potion: Potion | null
) {
const [
currentPotion,
setCurrentPotion
] = useState(potion);
const [
currentCharacterStats,
setCurrentCharacterStats
] = useState(character.stats); useEffect(() => {
if (currentPotion) {
setCurrentCharacterStats((prevStats) => ({
hp: prevStats.hp + currentPotion.stats.hp,
attack: prevStats.attack + currentPotion.stats.attack,
defence: prevStats.defence + currentPotion.stats.defence,
}));
} return () => {
setCurrentPotion(null);
};
}, [currentPotion]);};
We have added our useEffect to check if a currentPotion is used, and then to set the currentCharacter’s stats to update as per the passed potion’s stats.
I have deliberately expanded the updates made to the hp, attack and defence properties for the character to make things more verbose.
We have also added a cleanup function to always set the currentPotion to null after we are done, so that we can pass in the same type of potion again if needed. For example, if our character has two of the same potions and uses them both one after the other.
Otherwise useEffect would not detect a change to the dependency and won’t update the character’s stats if the same potion was passed twice (or more times) in a row.
But so far, we have not returned anything from this hook. We will need to return something for our use case, so that we can set the potion to be used from outside of the hook and capture the changes to the character stats as well.
We will therefore return the following two things at the end of our custom hook:
import React, { useState, useEffect } from "react";
import { Character, Potion } from "../App";
export default function usePotion(
character: Character,
potion: Potion | null
) { const [
currentPotion,
setCurrentPotion
] = useState(potion);
const [
currentCharacterStats,
setCurrentCharacterStats
] = useState(character.stats); useEffect(() => {
if (currentPotion) {
setCurrentCharacterStats((prevStats) => ({
hp: prevStats.hp + currentPotion.stats.hp,
attack: prevStats.attack + currentPotion.stats.attack,
defence: prevStats.defence + currentPotion.stats.defence,
}));
}
return () => {
setCurrentPotion(null);
};
}, [currentPotion]); return [currentCharacterStats, setCurrentPotion] as const;
};
The character’s stats (currentCharacterStats), and the setCurrentPotion state update function to update the potion from outside of the hook.
The complete code for our custom hook should like this:
So from our custom hook, we exported the character’s updated stats and a function to set the current potion to be used to change these stats.
This exported function is nothing but the “setState” method: setCurrentPotion, used to update the currentPotion state.
As currentPotion is listed as a dependency, it will alos make the useEffect in our custom hook fire.
Now onto using this hook in our App.tsx file.
Using your custom hook
First, go to your App.tsx file and add an import for our custom hook. To the code which we already have in it, add the following import:
import React, { useState } from 'react';
import usePotion from "./hooks/usePotion";interface Stats {
hp: number;
attack: number;
defence: number;
}export interface Potion {
name: string;
stats: Stats;
}export interface Character {
name: string;
stats: Stats;
potions: Array<Potion>;
}const Geralt: Character = {
name: "Geralt of Rivia",
stats: {
hp: 3200,
attack: 650,
defence: 880,
},
potions: [
{
name: "Full Moon",
stats: {
hp: 400,
attack: 0,
defence: 0,
},
},
{
name: "Thunderbolt",
stats: {
hp: 0,
attack: 400,
defence: 0,
},
},
],
};export default function App() { const [witcher, setWitcher] = useState(Geralt); return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<div>
<h3>Name: {witcher.name}</h3>
<div>
<h4>Health: {witcher.stats.hp}</h4>
</div>
<div>
<h4>Attack: {witcher.stats.attack}</h4>
</div>
<div>
<h4>Defence: {witcher.stats.defence}</h4>
</div>
<div>
{witcher.potions.map((potion) => (
<div key={potion.name}>
{potion.name}
</div>
))}
</div>
</div>
</div>
);
}
Great, now let us make an initial call to the custom hook.
...export default function App() {const [stats, setPotion] = usePotion(Geralt, null);
const [witcher, setWitcher] = useState(Geralt);return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<div>
<h3>Name: {witcher.name}</h3>
<div>
<h4>Health: {witcher.stats.hp}</h4>
</div>
<div>
<h4>Attack: {witcher.stats.attack}</h4>
</div>
<div>
<h4>Defence: {witcher.stats.defence}</h4>
</div>
<div>
{witcher.potions.map((potion) => (
<div key={potion.name}>
{potion.name}
</div>
))}
</div>
</div>
</div>
);
}
We pass null as the potion initially because we do not want to use any potion yet. Instead, it would be great if we could use a potion by clicking a button.
We will do exactly this next, and hence we will create a function to update the potion with the help of setPotion (extracted above).
If you refer to our exports from our usePotion hook then you will see that setPotion will basically update the currentPotion in our hook.
We will also add a button to fire this function:
...export default function App() {
const [stats, setPotion] = usePotion(Geralt, null);
const [witcher, setWitcher] = useState(Geralt); const potionHandler = (potion: Potion) => {
setPotion(potion);
}; return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<div>
<h3>Name: {witcher.name}</h3>
<div>
<h4>Health: {witcher.stats.hp}</h4>
</div>
<div>
<h4>Attack: {witcher.stats.attack}</h4>
</div>
<div>
<h4>Defence: {witcher.stats.defence}</h4>
</div>
<div>
{witcher.potions.map((potion) => (
<div key={potion.name} style={{ width: "100%" }}>
<button
onClick={() => potionHandler(potion)}
style={{ width: "100%", marginTop: '10px' }}
>
{potion.name}
</button>
</div>
))}
</div>
</div>
</div>
);
}
At this point, your app will look like this in the browser:
But of course, clicking the buttons will not do anything. This is because we are not using the updated stats we receive from our custom hook to update our actual character stats.
To do this, we will add the following useEffect, and list stats as its dependency:
import React, { useState, useEffect } from "react";...export default function App() {
const [stats, setPotion] = usePotion(Geralt, null);
const [witcher, setWitcher] = useState(Geralt); const potionHandler = (potion: Potion) => {
setPotion(potion);
}; useEffect(() => {
setWitcher((prevWitcher) => ({ ...prevWitcher, stats }));
console.log(stats);
}, [stats]); return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<div>
<h3>Name: {witcher.name}</h3>
<div>
<h4>Health: {witcher.stats.hp}</h4>
</div>
<div>
<h4>Attack: {witcher.stats.attack}</h4>
</div>
<div>
<h4>Defence: {witcher.stats.defence}</h4>
</div>
<div>
{witcher.potions.map((potion) => (
<div key={potion.name} style={{ width: "100%" }}>
<button
onClick={() => potionHandler(potion)}
style={{ width: "100%", marginTop: '10px' }}
>
{potion.name}
</button>
</div>
))}
</div>
</div>
</div>
);
}
So whenever we get new stats from our custom hook, the useEffect fires (since it has stats as a dependency) and updates our witcher state:
useEffect(() => {
setWitcher((prevWitcher) => ({ ...prevWitcher, stats }));
console.log(stats);
}, [stats]);
The complete code for App.tsx looks like this:
And there you have it, your application should now work as intended!