How I create my own React Hooks (and why you should, too!)
Intro
Hello Codementor! In this post, I will demonstrate how to create custom React Hooks to use in your React projects. Custom hooks increase code reusability, modularity and readability. I am going to be using React Context for state management and will be coding the example in Typescript, but if you use regular javascript, you can code along too. Just don't add in the typings. Let's go!
What are React Hooks?
React Hooks, from version 16.8 onwards, are functions that allow you to "hook" into the features of React, without having to write a class-based component. For example, the useState()
hook allows you to create a local state variable without having to use a React Component with a state object:
const [name, setName] = useState("Anonymous");
return (
<button onClick={()=>setName("Sean")}>Hello {name}!</button>
)
When the button is clicked, the component will rerender because the state of name
will change.
The most common two hooks are useState
and useEffect
. useEffect
is equivalent of componentDidUpdate
in class-based components, but can be modified to fire off only on specific changes in state, and not on every rerender.
React hooks can only be used within a React functional component. Trying to call useEffect
, for instance, outside a functional component, will throw you nasty errors.
There is one exception: A custom hook. When creating a custom hook, you can use all the functionality of react. You can use the library's built-in hooks, and even your own custom hooks. You don't have to return JSX at all, since it's not a functional component.
NB: a React custom hook's variable name MUST begin with use
. Let's go through an example below. I will be creating a Global State for a "Toast" alert which will pop up on certain user input, remain for a little bit and then disappear.
Basic Setup
First off, let's create a new app:
On your terminal,type:
npx create-react-app ts-test --template typescript
Assuming you have Nodejs installed on your machine, the terminal will do the rest. You will see a new folder ts-test. Let's open it up in our code editor (Visual Studio Code recommended!)
I like working with Styled Components, so let's install it
npm i styled-components @types/styled-components
I like to do a cleanup of the src
folder, so delete everything but App.tsx
, index.css
and index.tsx
.
First off, I like to change the index.css
file to this:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Then, let's create a styles.ts
file in src
:
import styled from 'styled-components';
const Container = styled.div`
width: 100vw;
height: 100vh;
text-align: center;
background: lightblue;
padding: 1rem;
`;
export { Container };
And here's how our App.tsx
will look for now:
import React from 'react';
import { ToastProvider } from './context';
import { Container } from './styles';
function App() {
return (
<ToastProvider>
<Container>
<h1>MY APP!</h1>
</Container>
</ToastProvider>
);
}
export default App;
As you see there, I have created a context for a Toast message that appears at the bottom of the screen. So create a context
folder, and inside that, index.ts
:
export * from './Toast';
This assumes a Toast
folder, so let's create it, then in that folder create an index.tsx
and a styles.ts
. The code for index.tsx
:
import React, { createContext, ReactNode, useEffect, useState } from 'react';
import { Container } from './styles';
interface ChildrenProps {
children: ReactNode;
}
interface ToastSchema {
type: 'WARNING' | 'SUCCESS';
message: string;
}
const ToastContext = createContext<
React.Dispatch<React.SetStateAction<ToastSchema | undefined>>
>(() => {});
const ToastProvider: React.FC<ChildrenProps> = ({ children }) => {
const [toast, setToast] = useState<ToastSchema | undefined>();
useEffect(() => {
if (toast) {
const timeoutId = setTimeout(() => setToast(undefined), 3000);
return () => {
clearTimeout(timeoutId);
};
}
}, [toast]);
return (
<ToastContext.Provider value={setToast}>
{children}
{toast && <Container type={toast.type}>{toast.message}</Container>}
</ToastContext.Provider>
);
};
export { ToastProvider, ToastContext };
and for styles.ts
:
import styled from 'styled-components';
const Container = styled.div<{ type: 'WARNING' | 'SUCCESS' }>`
position: fixed;
left: 50%;
bottom: 10%;
transform: translate(-50%, -50%);
background: ${({ type }) => (type === 'SUCCESS' ? 'green' : 'red')};
color: white;
font-weight: bold;
padding: 0.5rem;
min-width: 200px;
text-align: center;
border-radius: 5px;
`;
export { Container };
Let's see what's going on in the Toast component there. I am creating a context which accepts a React state action as a value. I am then wrapping essentially my entire app in this context and thus exposing the entire app to the setToast value there. the useEffect
will check if there's a toast, and then kick off a timer that will remove the toast after 3 seconds. Let's see if it works. Back in the src
folder, create a file called Component.tsx
:
import React, { useContext } from 'react';
import { ToastContext } from './context';
const Component = () => {
const setToast = useContext(ToastContext);
return (
<div>
<h1>MY COMPONENT!</h1>
<button
onClick={() => setToast({ type: 'SUCCESS', message: 'SUCCESS!' })}
>
Make a Successful Toast!
</button>
<button onClick={() => setToast({ type: 'WARNING', message: 'UH OH!!' })}>
Make an Error Toast!
</button>
</div>
);
};
export default Component;
and let's update App.tsx
import React from 'react';
import Component from './Component';
import { ToastProvider } from './context';
import { Container } from './styles';
function App() {
return (
<ToastProvider>
<Container>
<Component />
</Container>
</ToastProvider>
);
}
export default App;
Your folder structure should be as such:
If you run npm run start
, you should get this:
And that toast should be popping up depending on the button you pressed, and disappearing after 3 seconds.
Great!
The Value of a Custom Hook
Looking at the code in Component.tsx
, there's some troubling stuff. It seems every time I want to display an alert, I need to import useContext
, ToastContext
, declare the setToast
variable and set the type
and message
properties for each instance. This could get very cumbersome when, let's say for each form in my app, once it passes data to an API and receives a message, you want to show it as a toast. It's just a lot of seemingly unnecessary imports and lines of code.
Custom Hooks to the Rescue!
Let's write something nifty! In Toast/index.tsx
, add this code:
const useToast = () => {
const setToast = useContext(ToastContext);
const alertSuccess = (message: string) =>
setToast({ type: 'SUCCESS', message });
const alertError = (message: string) =>
setToast({ type: 'WARNING', message });
return { alertSuccess, alertError };
};
In this function, starting with use
, I am fetching the Toast context and creating two functions that area more lightweight and specific.
You can see that even though I am using a React Hook outside of a functional component, I am allowed to because this is a custom hook! I am then returning the two functions in an object, as a return value of the main function.
Remember to Export it along with the over variables at the bottom of the file. Now, let's refactor Component.tsx
and see what we've got:
import React from 'react';
import { useToast } from './context';
const Component = () => {
const { alertSuccess, alertError } = useToast();
return (
<div>
<h1>MY COMPONENT!</h1>
<button onClick={() => alertSuccess('SUCCESS!')}>
Make a Successful Toast!
</button>
<button onClick={() => alertError('ERROR!')}>Make an Error Toast!</button>
</div>
);
};
export default Component;
Now if you reload your app, you will see no change in functionality, but your code looks way better! And isn't that what we want?
Conclusion
In this post, we learned how to create custom React Hooks and implement them effectively. The sky's the limit as to where custom hooks can take you. Reply in the comments if you've made a cool one too!
Happy coding!
~ Sean