Building a photo gallery app from scratch with Chakra UI
Tags: react, javascript, webdev, featuredposts
Published: 2019-10-02 13:47:43

---
title: Building a photo gallery app from scratch with Chakra UI
published: true
tags: react,javascript,webdev,featuredposts
canonical_url: https://blog.logrocket.com/building-photo-gallery-app-from-scratch-chakra-ui/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/iru4vh9gxv2pnrrqhl9f.jpg
---
**Written by [Foysal Ahamed](https://blog.logrocket.com/author/foysalahamed/)**✏️

React is now one of the most battle-tested and mature front-end frameworks in the world, and Express.js is its counterpart among back-end/server frameworks. If you’re building an app today, you can’t pick a better duo than this. In this post, I will walk you through building a complete app using these two frameworks, plus Chakra UI for component styling.

Before we dive in, for the impatients like me, here’s the [entire codebase on GitHub](https://github.com/foysalit/logrocket-photato). Feel free to clone it and take it for a spin.

## Is this post for me?

As a self-taught full-stack dev, I always find myself scouring the web for blog posts/tutorials that build out entire applications from scratch and demonstrate one or several features commonly seen in real-life apps. This kind of post helps devs across a broad spectrum of skill sets and experience.

Beginners learn how to glue together new concepts they have learned recently and turn them into a complete and usable app. Devs with an intermediate level of knowledge can learn how to organize, structure, and apply best practices when building full-stack apps.

So, if you’re just getting into the JS ecosystem — or if you have already built one or two apps but sometimes get confused about whether you’re doing it right — this post is for you.

Having said that, to read and complete this tutorial in one sitting, you will need to have:

- A clear understanding of basic JS concepts and some familiarity with ES6 syntax
- Used React at least once and have some familiarity with common concepts such as states, components, renders, etc.
- Familiarity with the concept of REST APIs
- Used a relational database
- Used Node.js and Express.js for a web server app
- A working JS ecosystem set up on your machine, i.e., the latest versions of npm, Node.js, etc. installed

If you find yourself missing any of the above items, worry not! The web has plenty of content that will help you get started and prepared for this post.

Please note that my primary OS is Ubuntu, so all the commands in this post assume you have a \*nix system.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Laying the groundwork

Before starting any new project, it is easy to get impatient and start writing code right away. However, it is always a good idea to plan out your features and workflow first — at least that’s what I always do. So let’s make a plan for how our app will work.

Our app will have two main parts. One is the client-side React app that lets me upload photos through my browser. The uploaded photos are then shown in a gallery view.

The other part is a server-side API that receives a photo upload, stores it somewhere, and lets us query and display all the uploaded photos.

Before all that programming mumbo-jumbo, however, let’s give our app a catchy name. I’m calling it _photato_, but feel free to give it a better name yourself, and let me know what you come up with. [🙂](https://s.w.org/images/core/emoji/12.0.0-1/72x72/1f642.png)

OK, time to code. Let’s make container folders for our app first:

```jsx
mkdir photato && cd $_
mkdir web
mkdir api
```

We will start by creating our front-end React app. React comes with a handy tool that lets you bootstrap a React app real fast:

```jsx
cd web
npx create-react-app web
```

Now you should have a bunch of files and folders in the `web/` folder, and the output will tell you that by going into the directory and running `yarn start`, you can make your app available at `http://localhost:3000`.

![Create React App Default Files](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/create-react-app-generated-files.png?resize=205%2C400&ssl=1)

If you have built websites/web apps before, you might be familiar with the struggle of building UIs with raw HTML and CSS. UI libraries like Bootstrap, Semantic UI, Material Kit, and countless others have long been the saviors of full-stack devs who can’t produce “dribbble famous”-quality design.

In this post, we will look away from the more common, traditional UI libraries mentioned above and use [Chakra UI](https://chakra-ui.com/), built with accessibility in mind on the utility-first CSS framework [Tailwind CSS](https://tailwindcss.com/).

Following the Chakra UI get-started guide, run the following commands in your React app’s root directory:

```jsx
yarn add @chakra-ui/core @emotion/core @emotion/styled emotion-theming
```

Chakra UI allows you to customize its look and feel through theming very easily, but for this post, we will stick to its default styling.

The last thing we need before we can start coding is one more library to get a [pretty-looking gallery](https://github.com/neptunian/react-photo-gallery):

```jsx
yarn add react-photo-gallery
```

Our app’s code will be encapsulated within the `src/` directory, so let’s take a look at it. create-react-app gave us a bunch of files, and with the help of Chakra UI, we can basically get rid of all the CSS stuff. Remove the `App.css`, `index.css`, and `logo.svg` files:

```jsx
cd src
rm -r App.css index.css logo.svg
```

This gives us a clean base on which to start building. Now let’s look at our setup for the server API app. Navigate back to the `api/` folder and create a new file by running the following commands:

```jsx
cd ../../api
touch package.json
```

Now copy and paste the following code into the `package.json` file:

```jsx
{
  "name": "api",
  "version": "1.0.0",
  "description": "Server api for photato",
  "main": "dist",
  "author": "Foysal Ahamed",
  "license": "ISC",
  "entry": "src/index.js",
  "scripts": {
    "dev": "NODE_ENV=development nodemon src/index.js --exec babel-node",
    "start": "node dist",
    "build": "./node_modules/.bin/babel src --out-dir dist --copy-files",
    "prestart": "npm run -s build"
  },
  "eslintConfig": {
    "extends": "eslint:recommended",
    "parserOptions": {
      "ecmaVersion": 7,
      "sourceType": "module"
    },
    "env": {
      "node": true
    },
    "rules": {
      "no-console": 0,
      "no-unused-vars": 1
    }
  },
  "dependencies": {
    "cors": "^2.8.4",
    "express": "^4.13.3",
    "mysql2": "^1.6.1",
    "sequelize": "^5.18.4"
  },
  "devDependencies": {
    "@babel/cli": "^7.1.2",
    "@babel/core": "^7.1.2",
    "@babel/node": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.1.0",
    "@babel/preset-env": "^7.1.0",
    "eslint": "^3.1.1",
    "eslint-config-airbnb": "^17.1.0",
    "eslint-plugin-jsx-a11y": "^6.2.1",
    "nodemon": "^1.9.2"
  }
}
```

Notice that we have quite a few dev dependencies, and they are necessary to enable writing our app using the latest ES6 syntax transpiled through Babel.

Babel is a magnificent tool and full of wonderful features, but for our purposes, you need to know almost nothing about it. In our case, we just need to create a `.babelrc` file alongside the `package.json` file and put the following config in it:

```jsx
{
    "presets": [[
        "@babel/preset-env",
        {
            "targets": {
                "node": "current"
            }
        }
    ]],
    "plugins": [
        "@babel/plugin-proposal-class-properties"
    ]
}
```

There’s also a few other dependencies, like [Express](https://expressjs.com/) and [Sequelize](https://sequelize.org/), and we will see their usage later. That’s all the setup we need for our server app, but before we move on, let’s install all the packages by running the `npm install` command in the root of the `api/` folder. This command will generate a `node_modules/` folder and a `package.lock.json` file.

## Photo gallery with Chakra UI and React

We will start with the `App.js` file. Let’s clean up the generated code and fill it with the following code:

```jsx
import React from 'react';
import { ThemeProvider } from '@chakra-ui/core';

import AppContainer from './app.container';

function App() {
    return (
        <div>
            <ThemeProvider>
                <AppContainer />
            </ThemeProvider>
        </div>
    );
}

export default App;
```

This simplifies our entry component and delegates the actual logic to another container named `AppContainer`, which is wrapped within `ThemeProvider` from Chakra UI. The `ThemeProvider` component ensures all of its children can be styled with the Chakra UI theme or any custom theme you may want to pass to it.

With that out of the way, we will never have to touch `App.js` again. Let’s create the new file `touch src/app.container.js` and fill it with the following code:

```jsx
import React from 'react';
import PhotoGallery from 'react-photo-gallery';

import Header from './header.component';

function AppContainer() {
    const photos = [{
            src: 'http://placekitten.com/200/300',
            width: 3,
            height: 4,
        },
        {
            src: 'http://placekitten.com/200/200',
            width: 1,
            height: 1,
        },
        {
            src: 'http://placekitten.com/300/400',
            width: 3,
            height: 4,
        },
    ];

    return (
        <>
            <Header/>
            <PhotoGallery
                photos={photos}
            />
        </>
    );
}

export default App;
```

This component renders two other components, `Header` and `PhotoGallery`, where `PhotoGallery` is provided by the npm photo gallery lib. Note that we are passing a photos array containing placeholder images to the `PhotoGallery` component. We will get back to this later in the post and replace the heartwarming kitten photos with our own uploaded photos.

The other component, `Header`, is being imported from a file that doesn’t exist yet, so let’s create it: `touch src/header.component.js`. Put the following code in the file:

```jsx
import React from 'react';
import { Flex, Button, Text } from '@chakra-ui/core';

function Header ({
    isUploading = false, 
    onPhotoSelect,
}) {
    return (
        <Flex 
            px="4"
            py="4"
            justify="space-between"
        >
            <Text 
                as="div"
                fontSize="xl" 
                fontWeight="bold" 
            >
                <span 
                    role="img" 
                    aria-labelledby="potato"
                >
                    🥔 
                </span> 
                <span 
                    role="img" 
                    aria-labelledby="potato"
                >
                    🍠 
                </span> 
                Photato
            </Text>

            <Flex align="end">
                <Button 
                    size="sm"
                    variant="outline"
                    variantColor="blue"
                    isLoading={isUploading}
                    loadingText="Uploading..."
                >
                    Upload Photo     
                </Button>
            </Flex>
        </Flex>
    );
};

export default Header;
```

If you followed all the above steps, the app in your browser should render something like this:

![First Look At The App: Kitten Photos](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/first-look-with-kittens.png?resize=730%2C449&ssl=1)

Let’s break down what we’ve done so far.

The `Header` component wraps all its children in a Chakra UI `Flex` component that renders an HTML `div` element with CSS style `display: flex`. Being a utility-based CSS framework, Chakra UI allows you to pass various props to its components to style them to your liking, and you will see this used throughout our app.

In our wrapper `Flex` component, `px` and `py` props give it a nice horizontal and vertical padding (respectively), and the `justify="space-between"` prop ensures that the elements inside it are rendered with equal spacing between them. If you’re not very familiar with CSS flexbox, I highly encourage you to learn more about this amazing layout tool.

Inside the `Flex` container, we have a `Text` on the left of the screen and a `Button` for uploading new photos on the right of the screen. Let’s take a closer look at the `Button` here.

We use `size="sm"` to give it a smaller size, but you can play around with `lg`, `xs`, etc. values to change the size. The `variant="outline"` prop makes it a bordered button instead of filling it with color — and speaking of color, `variantColor="blue"` makes the border and the text blue. There are several other colors available out of the box from Chakra UI, and I would highly recommend [reading up on it](https://chakra-ui.com/color-mode).

So far, we have been focused on the looks. Let’s talk about the functionality. This component is a great example of one of the core principles of writing clean and easily maintainable front-end code. It’s a _dumb component_ that only renders the markup, and there is no logic being handled. To make it functional, we pass props to it from the parent. It expects two props:

- `isUploading`, which is a boolean and defaults to `false`. This prop determines the state of the **Upload Photo** button. When it is true, the button will go into a `loading` state to give the user a feedback that uploading is happening in the background.
- `onPhotoSelect`, which is a function that will be triggered when the user selects a new photo to upload. We will circle back to this later.

This way of writing components really helps you plan out the functionality and architecture one small chunk at a time. Without implementing the actual logic, we have already planned out how the button will work based on the requirements of our app.

We have a solid and functional base for our front-end app now, so let’s pause here for a moment and start setting up our back end.

## Server API

The entry point for our server api will be the `src/index.js` file, so let’s create that:

```jsx
mkdir src
touch index.js
```

Then put the following code in that file:

```jsx
import http from 'http';
import cors from 'cors';
import express from 'express';
import { Sequelize } from 'sequelize';

const config = {
    port: 3001,
    database: {
        username: "root",
        password: "admin",
        host: "localhost",
        port: "3306",
        dialect: "mysql",
        database: "photato",
    }
};

let app = express();
app.server = http.createServer(app);

// 3rd party middlewares
app.use(cors({}));

// connect to db
const database = new Sequelize(config.database);

database.sync().then(() => {
    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.server.listen(config.port, () => {
        console.log(`Started on port ${app.server.address().port}`);
    });
});

export default app;
```

This is a bare-bones setup; let’s break it down block by block.

```jsx
import http from 'http';
import cors from 'cors';
import express from 'express';
import { Sequelize } from 'sequelize';
```

Imports the necessary modules from Node’s built-in HTTP package and other third-party packages installed through npm.

```jsx
const config = {
    port: 3001,
    database: {
        username: "root",
        password: "admin",
        host: "localhost",
        port: "3306",
        dialect: "mysql",
        database: "photato",
    }
};
```

This defines configurations for the database and server port where the app will be available. You will need to change the database password and username based on your MySQL database setup. Also, make sure you create a new database schema named `photato` in your db.

Please note that in production-ready applications, you would pass the configs from env var instead of hardcoding them.

```jsx
let app = express();
app.server = http.createServer(app);

// 3rd party middlewares
app.use(cors({}));
```

This initializes the Express app and creates a server instance using Node’s `http.createServer` method. Express allows plugging in various functionalities through middlewares. One such middleware we are going to use enables CORS requests for our API.

Right now, we are allowing CORS requests from any origin, but you can add more fine-grained config to allow requests only from your front-end app’s domain name for security purposes.

```jsx
// connect to db
const database = new Sequelize(config.database);

database.sync().then(() => {
    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.server.listen(config.port, () => {
        console.log(`Started on port ${app.server.address().port}`);
    });
});
```

This initializes a Sequelize instance that connects to our MySQL database based on our config. Once the connection is established, it adds a handler for the `/` endpoint of our API that returns a JSON-formatted response. Then the app is opened up through the server port specified in the config.

We can now boot up our app and see what we have achieved so far. Run `npm run dev` in the `api/` folder and then go to `http://localhost:3001`. You should see something like this:

![First Look At Our API](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/api-first-look.png?resize=520%2C128&ssl=1)

Handling file uploads has a lot of edge cases and security concerns, so it’s not a very good idea to build it from scratch. We will use an npm package called [Multer](https://github.com/expressjs/multer) that makes it super easy. Install the package by running `npm i --save multer`, and then make the following changes in the `src/index.js` file:

```jsx
import http from 'http';
import cors from 'cors';
import multer from 'multer';
import { resolve } from 'path';

//previously written code here

const config = {
    port: 3001,
    uploadDir: `${resolve(__dirname, '..')}/uploads/`,
    database: {
        username: "root",
        password: "admin",
        host: "localhost",
        port: "3306",
        dialect: "mysql",
        database: "photato",
    }
};

//previously written code here

// connect to db
const database = new Sequelize(config.database);

// setup multer
const uploadMiddleware = multer({ 
    dest: config.uploadDir,
    fileFilter: function (req, file, cb) {
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
            return cb(new Error('Only image files are allowed!'));
        }
        cb(null, true);
    }, 
}).single('photo');

//previously written code here

    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.post('/photo', uploadMiddleware, async (req, res) => {
        try {
            const photo = await Photo.create(req.file);
            res.json({success: true, photo});
        } catch (err) {
            res.status(422).json({success: false, message: err.message});
        }
    });

//previously written code here
```

Overview of the additions:

- Imports Multer package
- Adds the destination directory where the uploaded files will be stored. Right now, it’s set to be `api/upload/`, which doesn’t exist yet. So let’s create the folder as well: `mkdir upload`
- Initializes the Multer middleware that accepts a single file with the key `photo` and saves the file in the specified folder
- Only allows image files to be uploaded through Multer
- Adds a new POST request endpoint that uses the upload middleware. Once the file is handled by the middleware, it attaches the file info, such as destination path, size, mimetype etc., to the Express req object that is passed to the next handler. In this case, the next handler _tries_ to save the file details in the database (we will discuss this soon), and on success, it returns a JSON response including the file details, and on failure, it returns a JSON response with the error message

This line `const photo = await Photo.create(req.file);`, however, needs a bit more explanation. `ModelName.create(modelData)` is how you create a new row in a database table through Sequelize, and in the above code, we expect a Sequelize model named `Photo` to exist, which we haven’t created yet. Let’s fix that by running `touch src/photo.model.js` and putting the following code in that file:

```jsx
import { Model, DataTypes } from 'sequelize';

const PhotoSchema = {
    originalname: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    mimetype: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    size: {
        type: DataTypes.INTEGER,
        allowNull: false,
    },
    filename: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    path: {
        type: DataTypes.STRING,
        allowNull: false,
    },
};

class PhotoModel extends Model {
    static init (sequelize) {
        return super.init(PhotoSchema, { sequelize });
    }
};

export default PhotoModel;
```

That’s a lot of code, but the gist of it is that we are creating a Sequelize model class with a schema definition where the fields (table columns) are all strings (translates to VARCHAR in MySQL) except for the size field, which is an integer. The schema looks like this because after handling uploaded files, Multer provides exactly that data and attaches it to `req.file`.

Going back to how this model can be used in our route handler, we need to connect the model with MySQL through Sequelize. In our `src/index.js` file, add the following lines:

```jsx
// previously written code
import { Sequelize } from 'sequelize';
import PhotoModel from './photo.model';

// previously written code

// connect to db
const database = new Sequelize(config.database);

// initialize models
const Photo = PhotoModel.init(database);

// previously written code
```

So now that we have pieced together the missing case of the `Photo`, let’s add one more endpoint to our API and see one more use of the model:

```jsx
// previously written code

    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.get('/photo', async (req, res) => {
        const photos = await Photo.findAndCountAll();
        res.json({success: true, photos});
    });

// previously written code
```

This adds a GET request handler at the `/photo` path and returns a JSON response containing all the previously uploaded photos. Notice that `Photo.findAndCountAll()` returns an object that looks like this:

```jsx
{
    count: <number of entries in the model/table>,
    rows: [
        {<object containing column data from the table>},
        {<object containing column data from the table>},
        ....
    ]
}
```

With all the above changes, your `src/index.js` file should look like this:

```jsx
import http from 'http';
import cors from 'cors';
import multer from 'multer';
import express from 'express';
import { resolve } from 'path';
import { Sequelize } from 'sequelize';

import PhotoModel from './photo.model';

const config = {
    port: 3001,
    uploadDir: `${resolve(__dirname, '..')}/uploads/`,
    database: {
        username: "root",
        password: "admin",
        host: "localhost",
        port: "3306",
        dialect: "mysql",
        database: "photato",
    }
};

let app = express();
app.server = http.createServer(app);

// 3rd party middlewares
app.use(cors({}));

// connect to db
const database = new Sequelize(config.database);

// initialize models
const Photo = PhotoModel.init(database);

// setup multer
const uploadMiddleware = multer({ 
    dest: config.uploadDir,
    fileFilter: function (req, file, cb) {
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
            return cb(new Error('Only image files are allowed!'));
        }
        cb(null, true);
    },
}).single('photo');

database.sync().then(() => {
    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.get('/photo', async (req, res) => {
        const photos = await Photo.findAndCountAll();
        res.json({success: true, photos});
    });

    app.post('/photo', uploadMiddleware, async (req, res) => {
        try {
            const photo = await Photo.create(req.file);
            res.json({success: true, photo});
        } catch (err) {
            res.status(400).json({success: false, message: err.message});
        }
    });

    app.server.listen(process.env.PORT || config.port, () => {
        console.log(`Started on port ${app.server.address().port}`);
    });
});

export default app;
```

You’ve come this far, congrats! Go grab a coffee or something refreshing and get ready to cross the finish line in style.

## Connect gallery with server API

At this point, we have two apps: one is a browser-based React app that runs on `http://localhost:3000`, and the other is a server-side Node.js app running on `http://localhost:3001`.

So far, however, they have been strangers to each other, living their own lives. So, naturally, the next step is to marry the two and hope that they live happily ever after!

We are going to use the browser’s [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to talk to our server app from the React app. To keep our server communication encapsulated, we will create a new file:

```jsx
cd ../web/
touch src/api.js
```

Then let’s add the following functions in that file:

```jsx
const API_URL = 'http://localhost:3001';

export async function getPhotos () {
    const response = await fetch(`${API_URL}/photo`);
    return response.json();
};

export async function uploadPhoto (file) {
    if (!file)
        return null; 

    const photoFormData = new FormData();

    photoFormData.append("photo", file);


    const response = await fetch(`${API_URL}/photo`, {
        method: 'POST',
        body: photoFormData,
    });

    return response.json();
};
```

Let’s break it down:

- We have a variable `API_URL` that points to the URL where our server app is available
- `getPhotos` makes a GET request to the `/photo` endpoint of our server and parses the response as JSON before returning it
- `uploadPhoto` receives a `file` parameter and builds a `FormData` object that can be used to POST the file to the `/photo` endpoint of our server. After sending the request, it parses the response as JSON and returns it

Let’s use these nifty little functions, shall we? Open up the `src/app.container.js` file and add the following new lines in it:

```jsx
import React, { useState } from 'react';
// previously written code...

import { uploadPhoto } from './api';

function AppContainer() {
    const [isUploading, setIsUploading] = useState(false);


    async function handlePhotoSelect (file) {
        setIsUploading(true);
        await uploadPhoto(file);
        setIsUploading(false);
    };

    return (
            // previously written code...
            <Header
                isUploading={isUploading}
                onPhotoSelect={handlePhotoSelect}
            />
            // previously written code...
    );
}
```

With the above changes, we have added state Hooks in our `App` component. If you’re not familiar with Hooks and states, I encourage you to [read up on it](https://dev.to/bnevilleoneill/state-management-using-only-react-hooks-5871), but in short, state lets you re-render your UI whenever your state value changes.

Whenever our function `handlePhotoSelect` is executed with a file argument, it will first change `isUploading`‘s value to `true`. Then it will pass the file data to our `uploadPhoto` function, and when that finishes, it will switch `isUploading`‘s value to `false`:

```jsx
<Header
    isUploading={isUploading}
    onPhotoSelect={handlePhotoSelect}
/>
```

Then, we pass our `isUploading` state as a prop to our header component — and, if you recall, when `isUploading` changes to `true`, our **Upload Photo** button will transition into a loading state.

The second prop `onPhotoSelect` gets the function `handlePhotoSelect`. Remember when we wrote our `Header` component we defined the `onPhotoSelect` prop but never used it? Well, let’s settle that by making the following changes in the `src/header.component.js` file:

```jsx
// previously written code...
function Header ({
    isUploading = false, 
    onPhotoSelect,
}) {
    let hiddenInput = null;

    // previously written code...

    return (
        // previously written code...
                <Button 
                    // previously written code...
                    onClick={() => hiddenInput.click()}
                >
                    Upload Photo     
                </Button>

                <input
                    hidden
                    type='file'
                    ref={el => hiddenInput = el}
                    onChange={(e) => onPhotoSelect(e.target.files[0])}
                />
        // previously written code...
    );
};
```

The above changes add a hidden file input element and store its reference in the `hiddenInput` variable. Whenever the `Button` is clicked, we trigger a click on the file input element using the reference variable.

From there on, the browser’s built-in behavior kicks in and asks the user to select a file. After the user makes a selection, the `onChange` event is fired, and when that happens, we call the `onPhotoSelect` prop function and pass the selected file as its argument.

This completes one communication channel between our front-end and back-end apps. Now, you should be able to follow the below steps and get a similar result along the way:

1. Go to `http://localhost:3000`
2. Open the developer tools and go to the **Network** tab
3. Click the **Upload Photo** button and select an image file from your local folders.
4. See a new POST request being sent to `http://localhost:3001/photos` and a JSON response coming back.

Here’s how mine looks:

![First Upload Attempt](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/first-upload-attempt.png?resize=730%2C319&ssl=1)

To verify that the upload worked, go into the `api/uploads` directory, and you should see a file there. Try uploading more photos and see if they keep showing up in that folder. This is great, right? We are actually uploading our photos through our React app and saving it with our Node.js server app.

Sadly, the last step to tie it all together is to replace those kitty cats with our uploaded photos. To do that, we need to be able to request the server for an uploaded photo and get the photo file back. Let’s do that by adding one more endpoint in the `api/src/index.js` file:

```jsx
// previously written code...
    app.get('/', (req, res) => {
        res.json({app: 'photato'});
    });

    app.get("/photo/:filename", (req, res) => {
        res.sendFile(join(config.uploadDir, `/${req.params.filename}`));
    });
// previously written code...
```

The new endpoint allows us to pass any string in place of `:filename` through the URL, and the server looks for a file with that name in our `uploadDir` and sends the file in the response. So, if we have a file named `image1`, we can access that file by going to `http://localhost:3001/photo/image1`, and going to `http://localhost:3001/photo/image2` will give us the file named `image2`.

That was easy, right? Now back to the front end. Remember how our initial boilerplate `photos` variable looked like? The data that we get from the server is nothing like that, right? We will fix that first. Go back to the `web/src/api.js` file and make the following changes:

```jsx
export async function getPhotos () {
    const response = await fetch(`${API_URL}/photo`);
    const photoData = await response.json();

    if (!photoData.success || photoData.photos.count < 1)
        return [];

    return photoData.photos.rows.map(photo => ({
        src: `${API_URL}/photo/${photo.filename}`,
        width: 1, 
        height: 1,
    }));
};
```

The extra lines are just transforming our server-sent data into a format that can be passed to our `PhotoGallery` component. It builds the `src` URL from the `API_URL` and the filename property of each photo.

Back in the `app.container.js` file, we add the following changes:

```jsx
import React, { useState, useEffect } from 'react';
// previously written code...

import { uploadPhoto, getPhotos } from './api';

function AppContainer() {
    const [isUploading, setIsUploading] = useState(false);
    const [photos, setPhotos] = useState([]);

    useEffect(() => {
        if (!isUploading)
            getPhotos().then(setPhotos);
    }, [isUploading]);


    // previously written code...
}
```

That’s it! That’s all you need to show the uploaded photos in the image gallery. We replaced our static `photos` variable with a state variable and initially set it to an empty array.

The most notable thing in the above change is the `useEffect` function. Every time `isUploading` state is changed, as a side effect, React will run the first argument function in the `useEffect` call.

Within that function, we check if `isUploading` is `false`, meaning that a new upload is either complete or the component is loaded for the first time. For only those cases, we execute `getPhotos`, and the results of that function are stored in the `photos` state variable.

This ensures that, besides loading all the previous photos on first load, the gallery is also refreshed with the newly uploaded photo as soon as the upload is complete without the need to refresh the window.

This is fun, so I uploaded four consecutive photos, and this is how my photato looks now:

![Multi-Upload Result](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/multi-upload-result.png?resize=730%2C692&ssl=1)

## UX tidbits

While we do have a functioning app that meets all the requirements we set out to build, it could use some UX improvements. For instance, upload success/error does not trigger any feedback for the user. We will implement that by using a nifty little `toast` component from Chakra UI.

Let’s go back to the `web/src/app.container.js`:

```jsx
// previously written code...
import PhotoGallery from 'react-photo-gallery';
import { useToast } from '@chakra-ui/core';
// previously written code...

    const [photos, setPhotos] = useState([]);
    const toast = useToast();

    async function handlePhotoSelect (file) {
        setIsUploading(true);

        try {
            const result = await uploadPhoto(file);
            if (!result.success)
                throw new Error("Error Uploading photo");


            toast({
                duration: 5000,
                status: "success",
                isClosable: true,
                title: "Upload Complete.",
                description: "Saved your photo on Photato!",
            });
        } catch (err) {
            toast({
                duration: 9000,
                status: "error",
                isClosable: true,
                title: "Upload Error.",
                description: "Something went wrong when uploading your photo!",
            });
        }

        setIsUploading(false);
    };
// previously written code...
```

With the above changes, you should get a little green toast notification at the bottom of your screen every time you upload a new photo. Also notice that in case of error, we are calling the toast with `status:"error"`, which will show a red toast instead of green.

This is how my success toast looks:

![Success Toast In Green](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/success-toast.png?resize=288%2C70&ssl=1)

The gallery is made up of thumbnails. Shouldn’t we be able to see the full image as well? That would improve the UX a lot, right? So let’s build a full-screen version of the gallery with the [react-images](https://github.com/jossmac/react-images) package.

Start by running `yarn add react-images` within the `web/` directory. Then, pop open the `src/app.container.js` file and add the following bits:

```jsx
import React, { useState, useEffect, useCallback } from 'react';
import Carousel, { Modal, ModalGateway } from "react-images";
// previously written code...

function AppContainer() {
    const [currentImage, setCurrentImage] = useState(0);
    const [viewerIsOpen, setViewerIsOpen] = useState(false);

    const openLightbox = useCallback((event, { photo, index }) => {
        setCurrentImage(index);
        setViewerIsOpen(true);
    }, []);

    const closeLightbox = () => {
        setCurrentImage(0);
        setViewerIsOpen(false);
    };

    // previously written code...
    return (
        // previously written code...
            <PhotoGallery
                photos={photos}
                onClick={openLightbox}
            />
            <ModalGateway>
                {viewerIsOpen && (
                    <Modal onClose={closeLightbox}>
                        <Carousel
                            currentIndex={currentImage}
                            views={photos.map(x => ({
                                ...x,
                                srcset: x.srcSet,
                                caption: x.title
                            }))}
                        />
                    </Modal>
                )}
            </ModalGateway>
        // previously written code...
    );
}
```

Here’s what the changes are doing:

- Imports the necessary components from `react-images` to show a full-screen gallery
- Initiates two state variables: `currentImage` and `viewerIsOpen`. We will see how they are used soon
- Creates a memoized callback function, `openLightbox`, that gets triggered when the user clicks on any of the photos from the photo gallery. When executed, the function sets `viewerIsOpen` to true and sets the index number of the photo that was clicked
- Another function, `closeLightbox`, is created that essentially closes the full-screen gallery
- In the render method, if `viewerIsOpen` is `true`, we render the modal lightbox containing the `Carousel` component from the react-images lib
- The `Modal` component receives the prop `onClose={closeLightbox}` so that the user can close the full-screen gallery
- We pass the `currentImage` index number to it so it knows which photo will be shown first. In addition, we transform all the photos from the gallery and pass them to the carousel so that the user can swipe through all the photos in full-screen mode

The end result:

![Full-Screen Photo Gallery](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/09/full-screen-gallery.png?resize=730%2C591&ssl=1)

## Closing remarks

What we have built throughout this journey is a complete and functional app, but there’s a lot of room for improvement. Architecture, file-folder structure, testability — all of these things should be considered for refactoring both our client- and server-side apps. I would like you to take this as homework and add unit and/or integration testing to the codebase.

Chakra UI is a promising new tool and has numerous components that are hard to cover in one post, so I highly encourage you to [go through its docs](https://chakra-ui.com/getting-started) to learn more.

These days, saving uploaded content on the same disk where your app is running is somewhat frowned upon. Luckily, Multer has a lot of handy third-party plugins that would allow you to upload files directly to external storage such as S3. If you ever deploy your server app on hosting services like ZEIT Now or Netlify, they will come in handy.
* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/building-photo-gallery-app-from-scratch-chakra-ui/).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [Building a photo gallery app from scratch with Chakra UI](https://blog.logrocket.com/building-photo-gallery-app-from-scratch-chakra-ui/) appeared first on [LogRocket Blog](https://blog.logrocket.com).