Custom Hooks
Custom Hooks allow you to easily reuse stateful logic between components.
Image being able to write a function with just your component logic code but without the UI (JSX) and then reuse that in several components...that is what a custom hook enables.
Hooks are a new addition in React 16.8
Background
- Before hooks, React had patterns to reuse stateful logic but none of the approaches worked well
- First, Higher-Order Components
- Then, Render Props
- After hooks, creating custom hooks is an ideal solution to reuse stateful logic
Why Hooks?
It’s hard to reuse stateful logic between components.
- reusable behavior
- current patterns: render props and higher-order components
- both patterns create "wrapper hell" where components are surrounded by providers, consumers, higher-order components, render props etc...
- Hooks allow you to reuse stateful logic without changing your component hierarchy
Best Practice
Custom hooks are now considered a best practice in the React community.
Prefer creating a hook
for reuseable logic over the render props
pattern or high-order components
where possible.
Where to Use
Building your own Hooks lets you extract component logic into reusable functions.
In Classes | With Hooks |
---|---|
Higher-Order Components, Render Props | Custom Hooks |
Traditionally in React, we’ve had two popular ways to share stateful logic between components: render props and higher-order components. Hooks solve many of the same problems without forcing you to add more components to the tree.
Demo
Start: Function Component Example with API Object
const baseUrl = 'http://localhost:3000';
const url = `${baseUrl}/photos?_page=1&_limit=10`;
function translateStatusToErrorMessage(status) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the photos.';
default:
return 'There was an error retrieving the photos. Please try again.';
}
}
function checkStatus(response) {
if (response.ok) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(
`logging http details for debugging: ${JSON.stringify(httpErrorInfo)}`
);
let errorMessage = translateStatusToErrorMessage(httpErrorInfo.status);
throw new Error(errorMessage);
}
}
function parseJSON(response) {
return response.json();
}
function delay(ms) {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
}
const photoAPI = {
getAll(page = 1, limit = 100) {
return (
fetch(`${url}?_page=${page}&_limit=${limit}`)
// .then(delay(600))
.then(checkStatus)
.then(parseJSON)
.catch((error) => {
let errorMessage = translateStatusToErrorMessage(error);
throw new Error(errorMessage);
})
);
},
};
function PhotoList() {
const [loading, setLoading] = React.useState(false);
const [photos, setPhotos] = React.useState([]);
const [error, setError] = React.useState(null);
React.useEffect(() => {
setLoading(true);
setError(null);
photoAPI
.getAll(1)
.then((data) => {
setPhotos(data);
setLoading(false);
})
.catch((error) => {
setError(error.message);
setLoading(false);
});
}, []);
if (error) {
return <div>{error}</div>;
} else if (loading) {
return <div>Loading...</div>;
} else {
return (
<ul>
{photos.map((photo) => {
return (
<li key={photo.id}>
<img src={photo.thumbnailUrl} alt={photo.title} />
<h3>{photo.title}</h3>
</li>
);
})}
</ul>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<PhotoList />);
Complete: Function Component Example with API Object & Custom Hook
const baseUrl = 'http://localhost:3000';
const url = `${baseUrl}/photos?_page=1&_limit=10`;
function translateStatusToErrorMessage(status) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the photos.';
default:
return 'There was an error retrieving the photos. Please try again.';
}
}
function checkStatus(response) {
if (response.ok) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(
`logging http details for debugging: ${JSON.stringify(httpErrorInfo)}`
);
let errorMessage = translateStatusToErrorMessage(httpErrorInfo.status);
throw new Error(errorMessage);
}
}
function parseJSON(response) {
return response.json();
}
function delay(ms) {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
}
const photoAPI = {
getAll(page = 1, limit = 100) {
return (
fetch(`${url}?_page=${page}&_limit=${limit}`)
// .then(delay(600))
.then(checkStatus)
.then(parseJSON)
.catch((error) => {
let errorMessage = translateStatusToErrorMessage(error);
throw new Error(errorMessage);
})
);
},
};
function usePhotos() {
const [loading, setLoading] = React.useState(false);
const [photos, setPhotos] = React.useState([]);
const [error, setError] = React.useState(null);
React.useEffect(() => {
setLoading(true);
setError(null);
photoAPI
.getAll(1)
.then((data) => {
setPhotos(data);
setLoading(false);
})
.catch((error) => {
setError(error.message);
setLoading(false);
});
}, []);
return { loading, photos, error };
}
function PhotoList() {
const { loading, photos, error } = usePhotos();
if (error) {
return <div>{error}</div>;
} else if (loading) {
return <div>Loading...</div>;
} else {
return (
<ul>
{photos.map((photo) => {
return (
<li key={photo.id}>
<img src={photo.thumbnailUrl} alt={photo.title} />
<h3>{photo.title}</h3>
</li>
);
})}
</ul>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<PhotoList />);
Rules of Hooks
- Only call hooks at the top level (of your function component)
- don't call them inside loops (for), conditions (if), or nested functions (only inside your main function component body)
- Only call hooks from React Functions
- call hooks from React function components
- call hooks from other custom hooks