Creating an AI photo generator and editing app with React
Tags: react, ai, mobile
Published: 2024-02-01 15:32:55

**Written by [Ibadehin Mojeed
](https://blog.logrocket.com/author/ibadehinmojeed/)✏️**

About a year ago, ChatGPT wowed the world with its ability to generate text content on demand. But generative AI goes beyond text — it can also edit and create images using text prompts. In this guide, I will show you how to build a web application using cloud-based AI with features like generating photorealistic images, removing backgrounds and objects from images, and facial restoration. 

In the process of building this application, you will learn how to leverage cloud-based AI models to create innovative web solutions. With these skills, you are set to build real-world, AI-driven applications and contribute to the evolving field of AI.

## Why cloud-based AI?

[Replicate](https://replicate.com/) lets you run machine learning models with a cloud API without having to understand the intricacies of machine learning or managing your own infrastructure. Their tools start at $0.00010/sec, with a free trial plan. 

Because Replicate is cloud-based and has a huge community, you can access over a thousand AI models that are dedicated to performing extraordinary things. Additionally, you don’t need to learn Python or R to build a model from scratch. Simply use your favorite language (JavaScript or TypeScript) to interact with these AI models. 

The application we will build in this tutorial will make use of the following AI models:

*   [stable-diffusion](https://replicate.com/stability-ai/stable-diffusion): A latent text-to-image diffusion model capable of generating photorealistic images given any text input
*   [object-removal](https://replicate.com/sujaykhandekar/object-removal): A model that removes specified objects from an image
*   [rembg](https://replicate.com/cjwbw/rembg): A model that removes the background from images
*   [gfpgan](https://replicate.com/tencentarc/gfpgan/examples): A practical face restoration model for old photos

If you're ready to unlock the potential of AI in image generation and editing, let's get started!

## Prerequisites

To follow this article, I recommend you have foundational experience with TypeScript, React, and HTML/CSS.

## Setting up our project

To begin, we will [set up our project using Vite](https://blog.logrocket.com/vite-adoption-guide/). To use Vite, first install Node on your machine if it's not already, then set up a React project with this command:

```bash
npm create vite@latest
```

Vite will prompt you to provide extra information like the project name, framework, and programming language to complete the operation. If successful, you should have something similar to the terminal output shown below: ![Project Terminal](https://blog.logrocket.com/wp-content/uploads/2024/01/project-terminal.png) Now, cd into the project root directory and run `npm install`. Afterwards, run `npm run dev` to serve your React app on a local host.

### Installing external dependencies

Next, we will install external dependencies for Ant Design and Axios. 

[Ant Design (antd)](https://ant.design/) is a React component library for building beautiful and modern user interfaces. It comes with a collection of prebuilt, enterprise-level UI components. To install Ant Design, use the command below:

```bash
npm install antd --save
```

Axios is a powerful [HTTP client library for making network requests](https://blog.logrocket.com/how-to-make-http-requests-like-a-pro-with-axios/) from your browser.

```bash
npm install axios
```

### Cleaning up the `App` component

By default, the `App` component has some code we don’t need. For now, replace it with the code block below:

```javascript
function App() {

  return <div>Hello world</div>
}

export default App
```

Additionally, delete the `App.css` file from your source folder.

### Updating default styling in `index.css`

In the `index.css` file, update the styling with the following:

```css
body {
    margin: 0;
    padding: 0;
}

.layout {
  height: 100vh;
}

.site-layout .site-layout-background {
  background: #fff;
}
```

We are replacing all the CSS styles that came with the application with ours so that we have full control over the application’s appearance. If you run the application again, you should see `Hello world` positioned at the top left of the web page.

## Setting up the application network layer

### Generating a Replicate API key

An API key is needed to access resources on Replicate’s server. To create one, [sign up here](https://replicate.com/).

### Defining the cloud AI models

The cloud AI models are identified by their unique IDs, which are constant. So, we will group them using an enum:

```javascript
export enum ReplicateAIModels {
    TextToImage = "2b017d9b67edd2ee1401238df49d75da53c523f36e363881e057f5dc3ed3c5b2",
    BgRemover = "fb8af171cfa1616ddcf1242c093f9c46bcada5ad4cf6f2fbe8b81b330ec5c003",
    ObjRemover = "153b0087c2576ad30d8cbddb35275b387d1a6bf986bda5499948f843f6460faf",
    FaceRestorer = "297a243ce8643961d52f745f9b6c8c1bd96850a51c92be5f43628a0d3e08321a"
}
```

### Generating an ImgBB API key

We need a temporary image service to host images that will be fed into the cloud AI models. Create an account on [ImgBB](https://imgbb.com/) to get a key that we will use to communicate with their API.

### Defining constant keys

Create a `constant.ts` file with the following constants:

```javascript
export const API_KEY = import.meta.env.VITE_REPLICATE_API_KEY
export const IMG_BB_API_KEY = import.meta.env.VITE_IMG_BB_API_KEY
export const BASE_URL = 'https://api.replicate.com/v1/'
export const PROXY_SERVER_URL = "https://proxy-server-407001.ue.r.appspot.com/"
export const IMGBB_URL = "https://api.imgbb.com/1/upload"
```

Because this project is hosted on GitHub, I am using the `import.meta.env` object from Vite to read environment variables. For testing purposes, you can include your keys. 

You might be wondering why we need both `BASE_URL` and `PROXY_URL`. In order to access resources on Replicate’s server from a web browser, we need to bypass the browser’s CORS policy with a proxy server, hence the `PROXY_SERVER_URL`. Feel free to test without the proxy URL to see the error for yourself.

### Implementing image upload

Within your source folder, create a `ImageHostingService.ts` file with the following code:

```javascript
export const uploadImageToImgBB = async (image: RcFile) => {
    try {
        const formData = new FormData();
        formData.append('image', image);

        const response = await axios.post(IMGBB_URL, formData, {
            params: {
                expiration: 600,
                key: IMG_BB_API_KEY,
            },
            headers: {
                'Content-Type': 'multipart/form-data'
            },
        });

        return response.data\["data"\]["image"]["url"]
    } catch (error) {
        if (axios.isAxiosError(error)) {
            console.error('Request failed with status code:', error.response?.status);
            console.error('Response data:', error.response?.data);
        }
        throw "Error uploading image"
    }
}
```

Within the `uploadImageToImgBB` function block, we use Axios to make a multi-part request for uploading an image to the ImgBB server. Once the upload is successful, we extract the image URL from the response body. In the next section, we will see where this function is called.

### Implementing the cloud AI API client

```javascript
import axios, {AxiosInstance} from 'axios';
import {ReplicateAIModels} from "./CloudAiModels.ts";
import {RcFile} from "antd/lib/upload";
import {uploadImageToImgBB} from "./ImageHostingService.ts";
import {delay} from "../common/Utils.ts";
import {PredictionResponse} from "./Response.ts";
import {API_KEY, BASE_URL, PROXY_SERVER_URL} from "./Constants.ts";

class ApiClient {
    private axiosInstance: AxiosInstance

    constructor(baseURL: string) {
        this.axiosInstance = axios.create({
            baseURL,
            headers: {
                'Content-Type': 'application/json',
                'Accept': "application/json",
                "Authorization": `Token ${API_KEY}`
            },
        })

        this.setUpPredictionStatusPolling()
    }

    private setUpPredictionStatusPolling() {
        this.axiosInstance.interceptors.response.use(async (response) => {
                console.log("Interceptor", response)

                const predictionId = response.data["id"]
                const predictionUrl = `predictions/${predictionId}`;

                let pollingStatus = response.data["status"]

                while (pollingStatus === "starting" || pollingStatus === "processing") {
                    await delay(3000)
                    response = await this.axiosInstance.get(predictionUrl)
                    pollingStatus = response.data["status"]
                    console.log("Interceptor polling status: ", pollingStatus)
                }

                return response;
            }, (error) => Promise.reject(error)
        )
    }

    public async generateImage(userPrompt: string): Promise<PredictionResponse> {
        const data = {
            "version": ReplicateAIModels.TextToImage,
            "input": {
                "prompt": userPrompt
            }
        };

        return await this.createPrediction(data)
    }

    public async removeBackgroundFromImage(image: RcFile) {

        const url = await uploadImageToImgBB(image)

        const data = {
            "version": ReplicateAIModels.BgRemover,
            "input": {
                "image": url
            }
        };

        return await this.createPrediction(data)

    }

    public async removeObjectFromImage(image: RcFile, objectToRemove: string) {

        const url = await uploadImageToImgBB(image)

        const data = {
            "version": ReplicateAIModels.ObjRemover,
            "input": {
                "image_path": url,
                'objects_to_remove': objectToRemove
            }
        };

        return await this.createPrediction(data)
    }

    public async restoreImage(image: RcFile) {

        const url = await uploadImageToImgBB(image)

        const data = {
            "version": ReplicateAIModels.FaceRestorer,
            "input": {
                "img": url,
                "scale": 1,
            }
        };

        return await this.createPrediction(data)
    }

    async createPrediction(data: object): Promise<PredictionResponse> {
         const response = await this.axiosInstance.post<PredictionResponse>("/predictions", data)
        return response.data
    }
}

const apiClient = new ApiClient(PROXY_SERVER_URL + BASE_URL)
export default apiClient
```

Pay attention to the `createPrediction` and `setUpPredictionStatusPolling` functions in the code block above, as they encapsulate the core business logic of the application. 

The function `createPrediction` submits an image processing request to Replicate’s server via the prediction endpoint. Because image processing takes time, the server returns a response immediately with a Job ID (Prediction ID), which we will use to query the Replicate server for the task status at a later time. 

Within `setUpPredictionStatusPolling`, we implement a polling logic that is common to all the image processing tasks we submit to Replicate’s server. Inside the response interceptor for the prediction endpoint, we query the server for the prediction status every three seconds. 

If the status isn't in the starting or processing phases, we return the response from the interceptor. By adopting this approach, we make sure there's a single point of logic handling both the polling of prediction status and retrieving the task result. 

Outside of the scope of the `ApiClient` class, we created an instance of the same class and made it visible to other files within our source code. It is a single instance of the API client, which will be shared by all the components that will be created in later sections.

## Setting up the UI layer

Having set up our core business logic, we are now going to create the user interfaces that will depend on them. As you already know, we will rely on React and UI components from Ant Design.

### Implementing the `Imagine` component

Within the `Imagine` component, we are going to implement the user interface for generative photo creation. With only text prompts, we’ll be able to generate realistic images. Hang tight, we’ll achieve that in a few lines of code:

```javascript
import {useState} from "react";
import {Flex, Image} from "antd";
import Search from "antd/lib/input/Search";
import apiClient from "../api/ReplicateApiClient.ts";

const Imagine = () => {

    const [imgSrc, setImgSrc] = useState("")
    const [loading, setLoading] = useState(false)

    function generateImage(query: string) {
        setLoading(true)
        apiClient.generateImage(query)
            .then((data) => {
                const imageUrl = data.output[0];
                setImgSrc(imageUrl)
                setLoading(false)
            })
            .catch(() => {
                setLoading(false)
            })
    }

    return (
        <Flex vertical={true} gap={10}>
            <Search
                style={{width: "500px"}}
                placeholder="Describe an image to generate"
                enterButton="Submit"
                disabled={loading}
                loading={loading}
                onSearch={(value) => {
                    (value.trim().length > 5) && generateImage(value)
                }}
            />

            {(imgSrc !== '') && (
                <Image
                    height="50%"
                    width="50%"
                    src={imgSrc}
                    alt="Your Image Alt Text"
                />
            )}
        </Flex>
    )
}

export default Imagine;
```

The `Imagine` function is composed of a `Search` input component and an `Image` component. Whenever `imgSrc` changes state based on user text input, React updates the `Image` component to reflect the latest image generated: ![A Generative AI Photo](https://blog.logrocket.com/wp-content/uploads/2024/01/generative-ai-photo.png)

### Implementing the `BackgroundRemove` component

The `BackgroundRemove` component encapsulates the logic for removing backgrounds from images. With one click, you can effortlessly remove distracting backgrounds from your photos without the hassle of an eraser tool. See the code block below for the implementation:

```javascript
import {useState} from 'react';
import {RcFile} from "antd/lib/upload";
import {isString} from "antd/es/button";
import {UploadOutlined} from "@ant-design/icons";
import {Button, Flex, Image, Upload} from "antd";
import apiClient from "../api/ReplicateApiClient.ts";

const BackgroundRemover = () => {

    const [image, setImage] = useState<RcFile>()
    const [imageAsUrl, setImageAsUrl] = useState("")
    const [editedImgSrc, setEditedImgSrc] = useState("")
    const [loading, setLoading] = useState(false)

    function handleChange(file: RcFile) {
        setImage(file)
        setImageAsUrl(URL.createObjectURL(file));
    }

    function removeImageBackground() {
        setLoading(true)
        apiClient.removeBackgroundFromImage(image as RcFile)
            .then((data) => {
                const imageUrl = data.output;
                if (isString(imageUrl)) setEditedImgSrc(imageUrl)
                setLoading(false)
            })
            .catch(() => {
                setLoading(false)
            });
    }

    return (

        <Flex vertical={true} gap={10}>

            <Flex vertical={false} gap={10}>

                <Upload
                    showUploadList={false}
                    disabled={loading}
                    beforeUpload={handleChange}>
                    <Button
                        disabled={loading}
                        icon={<UploadOutlined/>}
                        size={"middle"}>Import photo</Button>
                </Upload>

                {(imageAsUrl !== "") && <Button
                    type="primary"
                    disabled={loading}
                    loading={loading}
                    onClick={removeImageBackground}
                    size={"middle"}>
                    Remove Background
                </Button>}

            </Flex>

            <Flex vertical={false} gap={40}>

                {(imageAsUrl !== "") && (
                    <Image
                        height="40%"
                        width="40%"
                        src={imageAsUrl}
                        alt="Your Image Alt Text"
                    />
                )}

                {(editedImgSrc !== '') && (
                    <Image
                        height="40%"
                        width="40%"
                        src={editedImgSrc}
                        alt="Your Image Alt Text"
                    />
                )}
            </Flex>

        </Flex>

    )
}

export default BackgroundRemover
```

Ant Design’s `Upload` component responds to clicks by opening the file window for image selection. Within this flow, the selected file is passed to `handleChange(file)` in order to set the image for preview. 

Whenever `removeImageBackground()` is triggered from an `onClick()` button event, we invoke `removeBackgroundFromImage(image)` on the `apiClient` instance. The response from the method returns a promise with which we register a callback that will be triggered when the promise is fulfilled. Within this success callback, we extract `imageUrl` and update the state of `editedImgSrc`. 

As usual, React updates the DOM to show the selected photo with its background removed:

![Image with background removed](https://blog.logrocket.com/wp-content/uploads/2024/01/photo-background-removed.png)

### Implementing the `ObjectRemover` component

Using the [generative removal tool from Replicate](https://replicate.com/sujaykhandekar/object-removal), you can effortlessly remove unwanted objects from your image with only simple prompts. An example use case might be to remove a car from an otherwise perfect property photo. 

Within the `ObjectRemover` component, we will build the interface and business logic that allows for object removal:

```javascript
import {Button, Flex, Image, Upload} from "antd"
import {UploadOutlined} from "@ant-design/icons"
import {useState} from "react"
import {RcFile} from "antd/lib/upload"
import apiClient from "../api/ReplicateApiClient.ts"
import Search from "antd/lib/input/Search"
import {isString} from "antd/es/button"

const ObjectRemover = () => {

    const [image, setImage] = useState<RcFile>()
    const [imageAsUrl, setImageAsUrl] = useState("")
    const [editedImgSrc, setEditedImgSrc] = useState("")
    const [loading, setLoading] = useState(false)

    function handleChange(file: RcFile) {
        setImage(file)
        setImageAsUrl(URL.createObjectURL(file))
    }

    function removeObjectFromPhoto(objectDescription: string) {
        setLoading(true)
        apiClient.removeObjectFromImage(image as RcFile, objectDescription)
            .then((data) => {
                if (isString(data.output)) {
                    setEditedImgSrc(data.output)
                }
                console.log("response", data)
                setLoading(false)

            }).catch((error) => {
            console.error("error", error)
            setLoading(false)

        })
    }

    return (

        <Flex vertical={true} gap={10}>

            <Flex vertical={false} gap={10}>

                <Upload
                    showUploadList={false}
                    disabled={loading}
                    beforeUpload={handleChange}>
                    <Button
                        disabled={loading}
                        icon={<UploadOutlined/>}
                        size={"middle"}>Import photo</Button>
                </Upload>

                {(image !== null) && <Search
                    style={{width: "20vw"}}
                    placeholder="Objects(s) to remove"
                    enterButton="Submit"
                    disabled={loading}
                    loading={loading}
                    onSearch={(value) => {
                        (value.trim().length > 0) && removeObjectFromPhoto(value)
                    }}
                />

                }

            </Flex>

            <Flex vertical={false} gap={40}>

                {(imageAsUrl !== "") && (
                    <Image
                        height="40%"
                        width="40%"
                        src={imageAsUrl}
                        alt="Your Image Alt Text"
                    />
                )}

                {(editedImgSrc !== '') && (
                    <Image
                        height="40%"
                        width="40%"
                        src={editedImgSrc}
                        alt="Your Image Alt Text"
                    />
                )}
            </Flex>

        </Flex>
    )
}

export default ObjectRemover
```

The `ObjectRemover` component is composed of an `Upload` component for selecting an image and a `Search` component for capturing user prompts. In response to the imported image and user prompt, `apiClient.removeObjectFromImage(image, objectDescription)` is invoked with the appropriate arguments for requesting object removal. 

The result from the operation will be the input image minus the objects from the user prompt: ![Image With The Object Removed](https://blog.logrocket.com/wp-content/uploads/2024/01/object-removed.png)

### Implementing the `Restaurer` component

Restoring details on blurry photos can be a daunting and time-consuming task, but with cloud AI models, we can perform photo restoration in a fraction of the time. The `Restaurer` component contains logic to enable photo restoration in one click:

```javascript
import {useState} from 'react';
import {Button, Flex, Image, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import {RcFile} from "antd/lib/upload";
import apiClient from "../api/ReplicateApiClient.ts";
import {isString} from "antd/es/button";

const Restaurer = () => {

    const [image, setImage] = useState<RcFile>()
    const [imageAsUrl, setImageAsUrl] = useState("")
    const [editedImgSrc, setEditedImgSrc] = useState("")
    const [loading, setLoading] = useState(false)

    function handleChange(file: RcFile) {
        setImage(file)
        setImageAsUrl(URL.createObjectURL(file));
    }

    function restoreImage() {
        setLoading(true)
        apiClient.restoreImage(image as RcFile)
            .then((data) => {
                const imageUrl = data.output;
                if (isString(imageUrl)) setEditedImgSrc(imageUrl)
                setLoading(false)
            })
            .catch(() => {
                setLoading(false)
            })
    }

    return (

        <Flex vertical={true} gap={10}>

            <Flex vertical={false} gap={10}>

                <Upload
                    showUploadList={false}
                    disabled={loading}
                    beforeUpload={handleChange}>
                    <Button
                        disabled={loading}
                        icon={<UploadOutlined/>}
                        size={"middle"}>Import photo</Button>
                </Upload>

                {(imageAsUrl !== "") && <Button
                    type="primary"
                    disabled={loading}
                    loading={loading}
                    onClick={restoreImage}
                    size={"middle"}>
                    Restore Photo
                </Button>}

            </Flex>

            <Flex vertical={false} gap={40}>

                {(imageAsUrl !== "") && (
                    <Image
                        height="40%"
                        width="40%"
                        src={imageAsUrl}
                        alt="Original photo"
                    />
                )}

                {(editedImgSrc !== '') && (
                    <Image
                        height="40%"
                        width="40%"
                        src={editedImgSrc}
                        alt="Edited Photo"
                    />
                )}
            </Flex>

        </Flex>
    )
}

export default Restaurer
```

The code block is similar to that of the `RemoveBackground` component, the only difference being the method called on the `apiClient` instance. Successful execution of `restoreImage` on `apiClient` returns a new image with improvements to the originally imported photo. See the image below for a comparison: ![Image With Facial Restoration Applied](https://blog.logrocket.com/wp-content/uploads/2024/01/image-facial-restoration.png)

## Setting up the `App` component

The final piece of work involves a sidebar navigation menu within the `App` component. This setup enables a seamless switching between top-level components:

```javascript
import {BlockOutlined, BoldOutlined, FileImageOutlined, RotateLeftOutlined} from '@ant-design/icons'
import {Layout, Menu, MenuProps} from 'antd'
import {useState} from "react"
import Imagine from "./components/Imagine.tsx"
import ObjectRemover from "./components/ObjectRemover.tsx"
import BackgroundRemover from "./components/BackgroundRemover.tsx"
import Restaurer from "./components/Restaurer.tsx"

const {Sider, Content} = Layout

function App() {

    const menuItems = [
        {
            key: '1',
            icon: <FileImageOutlined/>,
            label: 'Imagine'
        },
        {
            key: '2',
            icon: <BlockOutlined/>,
            label: 'Obj Remover',
        },
        {
            key: '3',
            icon: <BoldOutlined/>,
            label: 'Bg Remover',
        },
        {
            key: '4',
            icon: <RotateLeftOutlined/>,
            label: 'Restaurer',
        },
    ]

    const sideBarComponents = [
        <Imagine/>,
        <ObjectRemover/>,
        <BackgroundRemover/>,
        <Restaurer/>
    ]

    const [itemId, setItemId] = useState("1")

    const onClickHandler: MenuProps['onClick'] = (e) => {
        setItemId(e.key)
    }

    return (
        <Layout className="layout">
            <Sider trigger={null} collapsible width={200}>
                <div className="logo">Cloud-IMG</div>
                <Menu
                    theme={"dark"}
                    onClick={onClickHandler}
                    mode="inline"
                    defaultSelectedKeys={['1']}
                    items={menuItems}
                />
            </Sider>
            <Layout className="site-layout">
                <Content
                    className="site-layout-background"
                    style={{
                        margin: '24px 16px',
                        padding: 24,
                    }}
                >
                    {sideBarComponents[parseInt(itemId) - 1]}

                </Content>
            </Layout>
        </Layout>
    )
}

export default App
```

The layout component defines a scaffold that hosts the `Sider` (the sidebar) and `Content` components. This simplifies web page setup for complex user interfaces. 

On the sidebar, whenever a menu item is clicked, the state of the `itemId` is set to the item’s key value. This change triggers React’s reconciliation process and updates the content component visually. 

With the `App` component fully set up, the web app should look like the image below: ![Final Web App](https://blog.logrocket.com/wp-content/uploads/2024/01/final-web-app.png)

## Demoing our application

Here’s a [video recording of the final application](https://www.youtube.com/watch?v=HA1IaMRFYgw&), demonstrating the different cloud AI use cases we covered in the article:    Feel free to explore the [live demo of the application](https://aimg-phi.vercel.app/).

## Conclusion

AI is here to stay, and cloud-based AI models have unlocked boundless possibilities for software applications. Through this article, we delved into the world of AI models, equipping you with the skills to build a remarkable photo editing application using React. 

Beyond this, I want to challenge you to explore other cloud-based AI models on Replicate and discover interesting project ideas you can build.

---
## Get set up with LogRocket's modern React error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)