Use React to Display Images in a Grid Like Google and Flickr
Tags: react, webdev, tutorial, javascript
Published: 2020-01-11 23:55:42

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

If you use image search websites like Google Image Search or Flickr, you will notice that their images display in a grid that looks like a wall of bricks. The images are uneven in height, but equal in width. This is called the masonry effect because it looks like a wall of bricks.

To implement the masonry effect, we have to set the width of the image proportional to the screen width and set the image height to be proportional to the aspect ratio of the image.

This is a pain to do if it’s done without any libraries, so people have made libraries to create this effect.

In this article, we will build a photo app that allows users to search for images and display images in a masonry grid. The image grid will have infinite scroll to get more images. We will build it with React and the React Masonry Component library. For infinite scrolling, we will use the React Infinite Scroller library. We will wrap the React Infinite Scroller outside the React Masonry Component to get infinite scrolling with the masonry effect when displaying images.

Our app will display images from the Pixabay API. You can view the API documentation and register for a key at [https://pixabay.com/api/docs/](https://pixabay.com/api/docs/)

To start, we run Create React App to create the app. Run `npx create-react-app photo-app` to create the initial code for the app.

Then we install our own libraries. We need React Infinite Scroller, React Masonry Component, Bootstrap for styling, Axios for making HTTP requests, Formik and Yup for form value data binding and form validation, and React Router for routing URLs to our pages.

To install all the packages, run:

```bash
npm i axios bootstrap formik react-bootstrap react-infinite-scroller react-masonry-component react-router-dom yup
```

to install all the packages.

With all the packages installed, we can start building the app. First start with replacing the code in `App.js` with:

```jsx
import React from "react";  
import { Router, Route } from "react-router-dom";  
import HomePage from "./HomePage";  
import { createBrowserHistory as createHistory } from "history";  
import TopBar from "./TopBar";  
import ImageSearchPage from "./ImageSearchPage";  
import "./App.css";  
const history = createHistory();

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/imagesearch" exact component={ImageSearchPage} />  
      </Router>  
    </div>  
  );  
}

export default App;
```

to add the top bar and the routes for our app into the entry point of the app.

Next remove all the code in `App.css` and add:

```css
.page {  
  padding: 20px;  
}
```

to add padding to our pages.

Then we set our React Masonry Component options by creating a `exports.js` in the `src` folder and add:

```javascript
export const masonryOptions = {  
  fitWidth: true,  
  columnWidth: 300,  
  gutter: 5  
};
```

**These options are very important.** We need to set `fitWidth` to `true` to center our grid. `columnWidth` must be a number to get constant width. It will scale according to screen size only with constant width. The `gutter` value is the margin between items.

The full list of options are at [https://masonry.desandro.com/options.html](https://masonry.desandro.com/options.html)

Next we create our app’s home page by creating `HomePage.js` in the `src` folder and add:

```jsx
import React from "react";  
import { getImages } from "./request";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import "./HomePage.css";  
import { masonryOptions } from "./exports";

function HomePage() {  
  const [images, setImages] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [initialized, setInitialized] = React.useState(false); 
  const getAllImages = async (pg = 1) => {  
    const response = await getImages(page);  
    let imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    pg++;  
    setPage(pg);  
  }; 

  React.useEffect(() => {  
    if (!initialized) {  
      getAllImages();  
      setInitialized(true);  
    }  
  }); 

  return (  
    <div className="page">  
      <h1 className="text-center">Home</h1>  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getAllImages}  
        hasMore={total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default HomePage;
```

In the home page, we just get the images when the page loads. When the user scroll down, we load more images by adding 1 to the current`page` value and get the image with the page number.

With the `InfiniteScroll` component, which is provided by React Infinite Scroll, wrapped outside the `Masonry` component, which is provided by React Masonry Component, we display our images in a grid, and also display more when the user scrolls down until the `length` of the `images` array is greater than or equal to the `total`, which is from the `total` field given by the Pixabay API’s results.

We load images when the page loads by checking if `initialized` flag is `true`, we only load images on page load if `initialized` is `false` and the when the request is first made to the API and succeeds, then we set `initialized` flag to `true` to stop requests from being made on every render.

Next we create a image search page by creating the `ImageSearchPage.js` file and adding the following:

```jsx
import React from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import { masonryOptions } from "./exports";  
import { searchImages } from "./request";

const schema = yup.object({  
  keyword: yup.string().required("Keyword is required")  
});

function ImageSearchPage() {  
  const [images, setImages] = React.useState([]);  
  const [keyword, setKeyword] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [searching, setSearching] = React.useState(false); 
  
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    setKeyword(evt.keyword);  
    searchAllImages(evt.keyword, 1);  
  }; 

  const searchAllImages = async (keyword, pg = 1) => {  
    setSearching(true); 
    const response = await searchImages(keyword, page);  
    let imgs = response.data.hits;  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  const getMoreImages = async () => {  
    let pg = page;  
    pg++;  
    const response = await searchImages(keyword, pg);  
    const imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  React.useEffect(() => {}); 
  
  return (  
    <div className="page">  
      <h1 className="text-center">Search</h1>  
      <Formik validationSchema={schema} onSubmit={handleSubmit}>  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="keyword">  
                <Form.Label>Keyword</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="keyword"  
                  placeholder="Keyword"  
                  value={values.keyword || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.keyword && errors.keyword}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.keyword}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Search  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      <br />  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getMoreImages}  
        hasMore={searching && total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default ImageSearchPage;
```

We do not load images on the first load on this page. Instead, the user enters a search term in the form and when the user clicks the Search button, then `handleSubmit` is called. The `evt` object has the form values, which is updated by the `Formik` component. Yup provides the form validation object with the `schema` object, we just check if `keyword` is required.

In the `handlesubmit` function, we get the `evt` object, which we validate against the schema by calling`schema.validate`, which returns a promise. If the promise returns to something truthy, then we proceed with making the request to the Pixabay API with the search keyword and page number.

We have the same setup as the home page for the infinite scroll and masonry effect image grid. The only difference is that we call the `searchAllImages` function which has similar logic as the `getAllImages` function, except that we pass in the `keyword` parameter in addition to the page parameter. We set the `imgs` variable to the array returned from the Pixabay API and set the `images` by calling `setImages`. We also set the page by calling `setPage`.

When the user scrolls far enough down that content runs out, the `getMoreImages` function is called when `images.length` is less than the `total`. The `total` is set by getting the `total` field from the API.

We use `masonryOptions` from `exports.js` just like in the home page and display images the same way.

Next create `request.js` in the `src` folder to add the code for making HTTP requests to the back end, like so:

```javascript
const axios = require("axios");  
const APIURL = "https://pixabay.com/api";

export const getImages = (page = 1) =>  
  axios.get(`${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}`);

export const searchImages = (keyword, page = 1) =>  
  axios.get(  
    `${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}&q=${keyword}`  
  );
```

We have the `getImages` for just getting images and `searchImages` that also sends the search term to the API. `process.env.REACT_APP_APIKEY` is from setting the `REACT_APP_APIKEY` variable in the `.env` file in the project’s root folder.

Next create `TopBar.js` in the `src` folder and add:

```jsx
import React from "react";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import { withRouter } from "react-router-dom";

function TopBar({ location }) {  
  React.useEffect(() => {}); 

  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">Photo App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={location.pathname == "/"}>  
            Home  
          </Nav.Link>  
          <Nav.Link  
            href="/imagesearch"  
            active={location.pathname == "/imagesearch"}  
          >  
            Search  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}  

export default withRouter(TopBar);
```

This contains the React Bootstrap `Navbar` to show a top bar with a link to the home page and the name of the app. We check the `location.pathname` to highlight the right links by setting the `active` prop, where the `location` prop is provided by React Router by wrapping the `withRouter` function outside the `TopBar` component.

Finally, in `index.js` , we replace the existing code with:

```html
<!DOCTYPE html>  
<html lang="en">  
  <head>  
    <meta charset="utf-8" />  
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />  
    <meta name="viewport" content="width=device-width, initial-scale=1" />  
    <meta name="theme-color" content="#000000" />  
    <meta  
      name="description"  
      content="Web site created using create-react-app"  
    />  
    <link rel="apple-touch-icon" href="logo192.png" />  
    <!--  
      manifest.json provides metadata used when your web app is installed on a  
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 
    -->  
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />  
    <!--  
      Notice the use of %PUBLIC_URL% in the tags above.  
      It will be replaced with the URL of the `public` folder during the build.  
      Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC\_URL%/favicon.ico" will  
      work correctly both with client-side routing and a non-root public URL.  
      Learn how to configure a non-root public URL by running `npm run build`.  
    -->  
    <title>Photo App</title>  
    <link  
      rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"  
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"  
      crossorigin="anonymous"  
    />  
  </head>  
  <body>  
    <noscript>You need to enable JavaScript to run this app.</noscript>  
    <div id="root"></div>  
    <!--  
      This HTML file is a template.  
      If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file.  
      The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`.  
      To create a production bundle, use `npm run build` or `yarn build`.  
    -->  
  </body>  
</html>
```

to add the Bootstrap CSS and change the title.