{"id":45,"date":"2025-07-01T14:26:02","date_gmt":"2025-07-01T18:26:02","guid":{"rendered":"https:\/\/www.shawntgray.com\/blog\/?p=45"},"modified":"2025-07-01T14:26:32","modified_gmt":"2025-07-01T18:26:32","slug":"how-to-build-an-art-gallery-with-react-part-4-filter-images","status":"publish","type":"post","link":"https:\/\/www.shawntgray.com\/blog\/how-to-build-an-art-gallery-with-react-part-4-filter-images\/","title":{"rendered":"How to Build an Art Gallery with React \u2013 Part 4: Filter Images"},"content":{"rendered":"\n<p>We&#8217;re half way there! In this part, we will:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use the data from <strong>images.json<\/strong> to build a full list of category tag buttons.<\/li>\n\n\n\n<li>Add tag buttons to the <strong>ImageCard<\/strong> components.<\/li>\n\n\n\n<li>Add functionality to the buttons that will filter the images by only displaying those images that include the selected category tags<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Add the Full List of Filter Buttons to App.js<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"113\" src=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-app-1024x113.png\" alt=\"Category Buttons for App Component\" class=\"wp-image-62\" srcset=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-app-1024x113.png 1024w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-app-300x33.png 300w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-app-768x85.png 768w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-app.png 1363w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Open your <strong>App.js<\/strong> file and update its content to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ imports\n\nfunction App() {\n  const images = data;\n  const uniqueCategories = &#91;...new Set(images.flatMap((image) => image.categories))].sort();\n\n  return (\n    &lt;div className='App'>\n      &lt;h1>Art Gallery&lt;\/h1>\n      &lt;p>Select categories to filter&lt;\/p>\n      &lt;div className='categories'>\n        {uniqueCategories.map((category) => (\n          &lt;button\n            key={category}\n            className={`category-pill ${filters.includes(category) ? 'active' : ''}`}\n          >{category}&lt;\/button>\n        ))}\n      &lt;\/div>\n      &lt;ImageGallery\n        images={images}\n      \/>\n    &lt;\/div>\n  );\n}\n\nexport default App;<\/code><\/pre>\n\n\n\n<p>Notes about the changes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We set our <strong><em>data <\/em><\/strong>to the <strong><em>images<\/em><\/strong> variable as the images\/image combo is a lot easier to understand at a glance than data\/datum.<\/li>\n\n\n\n<li>To gather all unique category tags from the<strong> images.json<\/strong> data, we create a new <em>Set <\/em>where we cycle through each image, gather all <strong><em>category <\/em><\/strong>tags, and add all unique tags to the <strong><em>uniqueCategories <\/em><\/strong>array ignoring any repeated tag.<\/li>\n\n\n\n<li>The <strong><em>uniqueCategories<\/em><\/strong> is alphabetized (A\u2192Z) for improved readability. If we didn&#8217;t use <strong><em>.sort()<\/em><\/strong>, the tags would be listed in a first-found order\u2014fairly confusing on the user&#8217;s end.<\/li>\n\n\n\n<li>We then create a new <strong><em>categories <\/em><\/strong>container in our <strong><em>App <\/em><\/strong>container where we map out each <strong><em>category <\/em><\/strong>as its own button.<\/li>\n<\/ul>\n\n\n\n<p>For now, the buttons don&#8217;t actually do anything. We&#8217;ll add the filter functionality later in this Part.<\/p>\n\n\n\n<p>Next, we&#8217;ll add the category tags to the individual <strong>ImageCard <\/strong>components in ImageCard.js. This won&#8217;t change your <strong>ImageGallery <\/strong>component as it already accepts your images and passes along each image&#8217;s information through the <strong>ImageCard <\/strong>element&#8217;s image attribute.<\/p>\n\n\n\n<p>Open your <strong>ImageCard.js<\/strong> file and update it to build the <strong><em>category <\/em><\/strong>tag buttons for each image.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"629\" height=\"415\" src=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-imagecard.png\" alt=\"Category Buttons for ImageCard component\" class=\"wp-image-63\" srcset=\"https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-imagecard.png 629w, https:\/\/www.shawntgray.com\/blog\/wp-content\/uploads\/2025\/06\/category-buttons-imagecard-300x198.png 300w\" sizes=\"auto, (max-width: 629px) 100vw, 629px\" \/><\/figure>\n\n\n\n<p><strong>ImageCard.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const ImageCard = (image) =&gt; {\n  return (\n    &lt;div className=\"image-card\"&gt;\n      &lt;img src={`${process.env.PUBLIC_URL}${image.src}`} alt={image.title} \/&gt;\n      &lt;p&gt;&lt;strong&gt;{image.title}&lt;\/strong&gt;&lt;\/p&gt;\n      &lt;div className=\"categories\"&gt;\n        {\n          image.categories.length &gt; 0\n            ? (\n              image.categories.map((category) =&gt; (\n                &lt;span\n                  key={category}\n                  className=\"category-pill\"\n                &gt;{category}&lt;\/span&gt;\n              ))\n            )\n            : (&lt;span&gt;Add a category&lt;\/span&gt;);\n        }\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n}\n\nexport default ImageCard;<\/code><\/pre>\n\n\n\n<p>What&#8217;s changed:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We created a <strong><em>categories<\/em> <\/strong>container within the <strong><em>image-card<\/em><\/strong> container. Note that this shares the same class name as the container you created in your <strong>App <\/strong>component. This way, all <strong><em>category <\/em><\/strong>tag buttons will be styled the same, signaling to the user that these buttons all serve the same purpose.<\/li>\n\n\n\n<li>We then set up a <strong>ternary statement<\/strong> (helpful shorthand for if\/else statements). If we have any categories listed for this image, build the buttons. Otherwise, display the message &#8220;Add a category&#8221;. The pattern for a <strong>ternary statement<\/strong> is:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code><em>condition<\/em>\n  ? <em>Do this if condition is true<\/em>\n  : <em>Do this if condition is false<\/em>;<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Here the <strong><em>category <\/em><\/strong>tag buttons are <strong>&lt;span&gt;<\/strong> elements, but they will function and be styled the same as the <strong>&lt;button&gt;<\/strong> elements in your <strong>App <\/strong>component.<\/li>\n<\/ul>\n\n\n\n<p>Now we&#8217;ll add the necessary styles for the <strong><em>categories <\/em><\/strong>and <strong><em>category-pill<\/em><\/strong> classes to <strong>App.css<\/strong>:<\/p>\n\n\n\n<p><strong>App.css<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/* All your previous styles *\/\n\n.categories {\n  margin-top: 10px;\n  margin-bottom: 20px;\n  display: flex;\n  flex-flow: row wrap;\n  gap: 1em;\n  justify-content: center;\n}\n\n.category-pill {\n  padding: 5px 12px;\n  background-color: #333;\n  color: #f1f1f1;\n  border: 1px solid #f1f1f1;\n  border-radius: 20px;\n  font-size: 14px;\n  font-weight: 500;\n  transition: all .3s, color .3s;\n  cursor: default;\n}\n\n.category-pill:hover,\n.category-pill.active {\n  background-color: #428645;\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Let&#8217;s Make Them Filter!<\/h2>\n\n\n\n<p>Now that all the buttons are in place, let&#8217;s make them actually filter your list of images. To do this, we&#8217;ll utilize React&#8217;s built-in <strong>useState <\/strong>functionality to track and, well, <em>react <\/em>to your changes.<\/p>\n\n\n\n<p>A <strong>useState <\/strong>statement takes in a variable and through some function, updates that variable, setting it to a new piece of data that reflects your changes.<\/p>\n\n\n\n<p>Remember that events bubble from the bottom up. Events update properties that are then fed from the top down. Knowing this, let&#8217;s add the filter functionality to the <strong>App <\/strong>component.<\/p>\n\n\n\n<p><strong>App.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { useState } from 'react';\n\/\/ other imports\n\nfunction App(){\n  const images = data;\n\n  const &#91;filters, setFilters] = useState(&#91;]);\n  const &#91;images, setImages] = useState(&#91;]);\n  \n  const toggleFilter = (category) =&gt; {\n    setFilters((prevFilters) =&gt; {\n      if (prevFilters.includes(category)) {\n        return prevFilters.filter((filter) =&gt; filter !== category);\n      } else {\n        return &#91;...prevFilters, category];\n      }\n    });\n  }\n\n  const uniqueCategories = &#91;...new Set(images.flatMap((image) =&gt; image.categories))].sort();\n\n  const filteredImages = filters.length === 0\n    ? images\n    : images.filter((image) =&gt; filters.every((filter) =&gt; image.categories.includes(filter)));\n  \n  return (\n    &lt;div className='App'&gt;\n      &lt;h1&gt;Art Gallery&lt;\/h1&gt;\n      &lt;p&gt;Select categories to filter&lt;\/p&gt;\n      &lt;div className='categories'&gt;\n        {uniqueCategories.map((category) =&gt; (\n          &lt;button\n            key={category}\n            className={`category-pill`}\n            onClick={() =&gt; toggleFilter(category)}\n          &gt;{category}&lt;\/button&gt;\n        ))}\n      &lt;\/div&gt;\n      {\n         filteredImages.length === 0\n           ? (\n             &lt;p style={{ color: 'red' }}&gt;&lt;em&gt;No images found for the selected categories.&lt;\/em&gt;&lt;\/p&gt;\n           )\n           : (\n             &lt;ImageGallery\n               images={filteredImages}\n               toggleFilter={toggleFilter}\n             \/&gt;\n           )\n      }\n    &lt;\/div&gt;\n  );\n}\n\nexport default App;<\/code><\/pre>\n\n\n\n<p>What is happening with these changes?<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Import the <strong><em>useState <\/em><\/strong>method from the React library.<\/li>\n\n\n\n<li>Instantiate <strong><em>useState <\/em><\/strong>methods as empty arrays for both <strong><em>filters <\/em><\/strong>and <strong><em>images<\/em><\/strong>.<\/li>\n\n\n\n<li>Build a <strong>toggleFilter <\/strong>function that takes in a category parameter and checks if the <strong><em>category <\/em><\/strong>already appears in the <strong><em>filters <\/em><\/strong>array. If so, remove it from the <strong><em>filters <\/em><\/strong>array. Otherwise, add the chosen <strong><em>category <\/em><\/strong>to the <strong><em>filters <\/em><\/strong>array.<\/li>\n\n\n\n<li>Build a <strong>filteredImages <\/strong>function that takes takes in the array of <strong><em>filters<\/em><\/strong>. If the array is empty, display all images. Otherwise, only display those images that have the categories that appear inside the <strong><em>filters <\/em><\/strong>array within their categories list (as shown in your <strong>images.json<\/strong> file).<\/li>\n\n\n\n<li>Update the <strong>&lt;button&gt;<\/strong> element built in the <strong><em>uniqueCategories.map()<\/em><\/strong> function by adding an <strong><em>onClick <\/em><\/strong>event that triggers the <strong><em>toggleFilter() <\/em><\/strong>function and pass it&#8217;s corresponding category to that function.<\/li>\n\n\n\n<li>Update the <strong>&lt;ImageGallery&gt;<\/strong> element so that it:\n<ul class=\"wp-block-list\">\n<li>Only displays images returned by the <strong><em>filterImages <\/em><\/strong>function.<\/li>\n\n\n\n<li>Passes along the <strong><em>toggleFilter <\/em><\/strong>function so that the <strong>ImageCard <\/strong>can inherit and use the function. Otherwise, <strong><em>toggleFilter <\/em><\/strong>will have to be redefined in the <strong>ImageCard <\/strong>component, which would cause a whole host of timing issues and variable\/event collisions that would outright destroy your app.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Next, update the <strong>ImageGallery <\/strong>component so it can successfully inherit and pass along the new <strong><em>toggleFilter <\/em><\/strong>function.<\/p>\n\n\n\n<p><strong>ImageGallery.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import ImageCard from '.\/ImageCard';\n\nconst ImageGallery = ({ images, toggleFilter }) =&gt; {\n  return (\n    &lt;div className=\"gallery\"&gt;\n      {images.map((image, i) =&gt; (\n        &lt;ImageCard\n          key={i}\n          image={image}\n          toggleFilter={toggleFilter}\n        \/&gt;\n      ))}\n    &lt;\/div&gt;\n  );\n}<\/code><\/pre>\n\n\n\n<p>Since the <strong>ImageGallery <\/strong>component basically serves simply as a throughline that connects the individual <strong>ImageCard <\/strong>components with the larger <strong>App <\/strong>component, the changes we made are simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><em>toggleFilter <\/em><\/strong>was added to the list of parameters <strong>ImageGallery <\/strong>can accept.<\/li>\n\n\n\n<li>The <strong>&lt;ImageCard&gt;<\/strong> element now has a new <strong>toggleFilter <\/strong>attribute set to <strong><em>toggleFilter<\/em><\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>Now, update the <strong>ImageCard <\/strong>component so it can properly inherit <strong><em>toggleFilter <\/em><\/strong>and use the function for its own group of filter buttons.<\/p>\n\n\n\n<p><strong>ImageCard.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const ImageCard = ({ image, toggleFilter }) =&gt; {\n  return (\n    &lt;div className=\"image-card\"&gt;\n      &lt;img src={`${process.env.PUBLIC_URL}${image.src}`} alt={image.title} \/&gt;\n      &lt;p&gt;&lt;strong&gt;{image.title}&lt;\/strong&gt;&lt;\/p&gt;\n      &lt;div className=\"categories\"&gt;\n        {\n          image.categories.length &gt; 0\n            ? (\n              image.categories.map((category) =&gt; (\n                &lt;span\n                  key={category}\n                  className=\"category-pill\"\n                  onClick={\n                    (e) =&gt; {\n                      e.stopPropagination();\n                      toggleFilter(category)\n                    }\n                  }\n                &gt;{category}&lt;\/span&gt;\n              ))\n            )\n            : (&lt;span&gt;Add a category&lt;\/span&gt;);\n        }\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n}\n\nexport default ImageCard;<\/code><\/pre>\n\n\n\n<p>All the heavy lifting is done within your <strong>App <\/strong>component. Here, we made two changes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>ImageCard <\/strong>contains a new parameter to properly inherit <strong><em>toggleFilter <\/em><\/strong>from its parent component.<\/li>\n\n\n\n<li>Each <strong>&lt;span&gt;<\/strong> created by <strong><em>image.categories.map()<\/em><\/strong> now contains an <strong><em>onClick <\/em><\/strong>event that fires off the <strong><em>toggleFilter <\/em><\/strong>function.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Filtering Functionality is Finished! *Phew*<\/h2>\n\n\n\n<p>We first added the category filter buttons to the <strong>App <\/strong>and <strong>ImageCard <\/strong>components. These buttons are dynamically built using the <strong>images.json<\/strong> data, <em>no hard-coded tags necessary<\/em>. This allows us to add, remove and edit categories on the fly without ever touching the app itself.<\/p>\n\n\n\n<p>Then, we built a series of <strong><em>useState <\/em><\/strong>methods and functions that capture categories when the buttons are clicked. We applied the functions to <strong>App<\/strong>, sent them through <strong>ImageGallery <\/strong>so they can eventually be inherited and utilized by the <strong>ImageCard <\/strong>component.<\/p>\n\n\n\n<p><strong>In the next part,<\/strong> we&#8217;ll add more functionality to allow the user to click an image to see it full-screen. We&#8217;ll also add a fun function that shuffles the images every time the app is loaded.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Add crucial interactivity to your Art Gallery React App by building filter functionality. Dynamically create filter buttons by pulling data from your JSON.<\/p>\n","protected":false},"author":1,"featured_media":34,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[9],"tags":[],"class_list":["post-45","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-art-gallery"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/45","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/comments?post=45"}],"version-history":[{"count":4,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/45\/revisions"}],"predecessor-version":[{"id":65,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/posts\/45\/revisions\/65"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/media\/34"}],"wp:attachment":[{"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/media?parent=45"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/categories?post=45"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.shawntgray.com\/blog\/wp-json\/wp\/v2\/tags?post=45"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}