Hey all! How would you build an image gallery with React Hooks? Here's how I would do it! (You can use here! and Edit Here!)
The main things to look at:
- This is written in Typescript, to assist the gist also contains the same code in JavaScript.
- The types!
- Our gallery starts with an object for each
Image
, here containing the basic info of aurl
and atitle
. - We can search for images by an arbitrary tag, and, and our images come from the server as an
ImageResponse
. This contains an array ofimages
, the searchedtag
, and a number representing the total number of pages available (totalPages
). - We represent a cache of the searched tags and the returned images with the
TaggedImages
type. This is aRecord
, which is an Object where the keys are the tag strings and the values are arrays of the returnedImage
arrays, indexed by page number.
- Our gallery starts with an object for each
- Our
useGallery
hook calls a few important hooks:- First: It calls
useState
to track thepageNumber
andtag
. - Second: It calls
useReducer
to create aTaggedImages
cache, and a function to update it with anImageResponse
. - Third: It calls
useEffect
, and in the effect it fetches the images for the gallery'scollectionUrl
,tag
, andpageNumber
. We pass a booleanshouldLoad
in addition to those dependencies in the effect's dependency array, to indicate the presence of a cached value and if we should load the data when the effect is run. Once the data loads, we get anImageResponse
and send it through our reducer!
- First: It calls
The example app in the CodeSandbox doesn't implement a loading indicator, why not see if you can fork it and implement one! If galleries or kittens aren't your thing, but you like this style, leave a comment with what hooks snippet I should write next!
import { useState, useReducer, useEffect } from 'react' | |
export type Image = { | |
url: string | |
title: string | |
} | |
export type ImageResponse = { | |
tag: string | |
images: Image[] | |
totalPages: number | |
} | |
type TaggedImages = Record<string, Image[][]> | |
export function useGallery( | |
collectionUrl: string, | |
initialTag: string, | |
initialPageNumber: number = 0 | |
) { | |
const [pageNumber, updatePageNumber] = useState<number>( | |
initialPageNumber, | |
) | |
const [tag, updateTag] = useState<string>(initialTag) | |
const [taggedImages, updateImages] = useReducer( | |
(taggedImages: TaggedImages, resp: ImageResponse) => { | |
const pages = ( | |
taggedImages[resp.tag] || new Array(resp.totalPages) | |
).slice() | |
pages[pageNumber] = resp.images | |
return { | |
...taggedImages, | |
[resp.tag]: pages, | |
} | |
}, | |
{} as TaggedImages, | |
) | |
const [loading, updateLoading] = useState<boolean>(true) | |
const [error, updateError] = useState<string>() | |
const shouldLoad = | |
!taggedImages[tag] || !taggedImages[tag][pageNumber] | |
useEffect(() => { | |
if (shouldLoad) { | |
updateLoading(true) | |
fetch(`${collectionUrl}?tag=${tag}&page=${pageNumber}`) | |
.then(resp => resp.json()) | |
.then((data: any) => { | |
updateLoading(false) | |
updateImages(data as ImageResponse) | |
}) | |
.catch(e => { | |
updateLoading(false) | |
updateError(e.toString()) | |
}) | |
} else { | |
updateLoading(false) | |
} | |
}, [shouldLoad, collectionUrl, tag, pageNumber]) | |
const pages = taggedImages[tag] | |
const images = pages && pages[pageNumber] ? pages[pageNumber] : [] | |
return { | |
images, | |
totalPages: pages ? pages.length : 0, | |
loading, | |
error, | |
updatePageNumber, | |
updateTag: (tag: string, pageNumber: number = 0) => { | |
updatePageNumber(pageNumber) | |
updateTag(tag) | |
}, | |
tag, | |
pageNumber, | |
} | |
} |
Top comments (0)