This is my first post here, so go easy on me 😅
Essentially, a hook is just a javascript function prefixed with use
which is allowed to invoke other hooks.1 Within reason, you can make a custom hook do whatever you want. Network requests, state management, redirecting the user, invoking other hooks, etc. You can get really creative with them.
Over the past few months, one of my favorite ways to use hooks has been to abstract out complicated component logic.
What do I mean by this?
Imagine you have a Newsfeed component. It loads and renders posts from your api. The simplest implementation is to inline everything at the top of your component:
// Newsfeed.jsx
import React, { useState } from 'react'
const Newsfeed = () => {
const [loading, setLoading] = useState(false)
const [posts, setPosts] = useState([])
const getPosts = () => {
setLoading(true)
fetch('/v1/posts')
.then(data => data.json())
.then(data => setPosts(data))
.then(() => setLoading(false))
}
return (
<div>
{posts.map((post, index) => (
<div key={index}>
<h1>{post.title}</h1>
</div>
))}
{loading ? (
<p>Loading...</p>
) : (
<button onClick={getPosts}>Load Posts</button>
)}
</div>
)
}
export default Newsfeed
The problem
The above implementation works, but I want you to imagine for a moment that suddenly instead of two states to manage, we have 5. We've got loading
, posts
, page
, perhaps we've got open
to track whether or not the user clicked into a post, and even replying
to track if they're replying to that post. Instead of just getPosts
, we have a bunch of different functions. Say, getPost
, getPostComments
, postComment
, etc.
That's a lot of complexity to add to the top of a functional component. It's an unmaintainable amount of complexity. That isn't even considering how the render portion of that component will grow in complexity, too. Sometimes you can break one complex component into many smaller components, but you can't always cleanly separate the logic.
A solution
Just don't include the functional complexity in the component.
Take all of it, and isolate it into a hook.
Why a hook? Because the integration is easy and seamless. No need to set up reducers or contexts. You get lots of bonuses like being able to call other hooks or automatic rerenders when your state updates.
The most important concept is that our hook, when used like this, must return everything required for the component to render. You can almost think of the return value of the hook as props being passed to the component.2
Let's see what it looks like after we import the hook, destructure the values returned, and ctrl+x and ctrl+v all of the logic away.
// Newsfeed.jsx
import React from 'react'
import useNewsfeed from './useNewsfeed'
const Newsfeed = () => {
// Destructure the value that the hook returns
const {
state: { loading, posts },
getPosts
} = useNewsfeed()
return (
<div>
{posts.map((post, index) => (
<div key={index}>
<h1>{post.title}</h1>
</div>
))}
{loading ? (
<p>Loading...</p>
) : (
<button onClick={getPosts}>Load Posts</button>
)}
</div>
)
}
export default Newsfeed
// useNewsfeed.js
import { useState } from 'react'
export default () => {
// Our hook manages our state for us
const [loading, setLoading] = useState(false)
const [posts, setPosts] = useState([])
// It also manages our functionality
const getPosts = () => {
setLoading(true)
fetch('/v1/posts')
.then(data => data.json())
.then(data => setPosts(data))
.then(() => setLoading(false))
}
// Finally, it exposes only what is required by the component
return {
state: { loading, posts },
getPosts,
}
}
Should you do this?
The answer is... it depends. It's like asking if you should inline a styling rule or if you should put it into a stylesheet. There are valid situations for both.
There are a couple of benefits to consider:
It cleanly separates your concerns without adding much complexity.3
It cleans up your imports a lot. You don't have 20 imports from a component library inbetween your network request imports.
Legibility. You can take one glance at the component (or the hook!) and you understand what's going on.
It tends to consolidate logic into one location, which makes locating, understanding, and altering it easier.
Seamless integration. No need to refactor anything.
You can completely forgoe the implementation. Our hook could look like this and our component would be none the wiser:
// useNewsfeed.js
// This always returns a fixed value, meaning
// the component always behaves as if the posts loaded already.
export default () => {
return {
state: {
loading: false,
posts: [{
id: 6,
title: 'Hooks are cool'
}]
},
getPosts: () => null,
}
}
Some very important caveats
This doesn't replace the need for separating components into multiple smaller components.
If you choose to do this, you really should be comfortable with how hooks work.
You should also familiarize yourself with how hook dependencies work. For example, if you don't properly use things like useCallback
and useMemo
, you might end up with infinite loops and not understand why. 😅
If you haven't, I recommend you download an extension that warns you when you're using them incorrectly to spot things like missing hook dependencies.
🧠What do you think?
Do you already use hooks like this?
Do you hate the idea of this?
Do you take it further and create tons of custom hooks?
Lemme know below 👋
1 https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook
2 Similarly, but not exactly the same. Remember, unlike props, React is choosing when to rerender everything based on certain things happening under the hood - like the value of useState
or a dependency passed to useCallback
changing.
3 With a simple example like our Newsfeed app that only has two states and one function, it probably isn't worth the complexity this adds. It's up to you to decide what's right. Always ask yourself: "Can I quickly develop and maintain a mental model of this?"
Top comments (3)
Great first post ;)
I used this method to retrieve data from an API and its worked perfectly. Thanks
Amazing! I don't use React in web but I'm learning React Native and I'll use this to develop my apps. Thank you!