How To Build A Wicked Cool App With React + Flask

by kleamerkuri

Hi there friends, today we’re embarking on a thrilling journey to create PinkPanda PDF–a React-Flask application that allows you to search through PDFs.

It’s going to be an application packed full of interesting components and functions like:

  • a custom drag-and-drop file input
  • uploading animations
  • PDF parsing and reading
  • pan and zoom action on search results

A killer React and Flask combo, I’d say.

Even though I created it, you can thank DM for bugging me to build something for his “damn classwork”. DM’s ease in life speaks more work for me 😒

But what developer can resist the challenge?

We’ll conclude this React-Flask project by deploying the React front-end on GitHub pages and the Flask back-end on Render.

Get ready to flex those coding muscles as we break down the process step by step!

Building the React front-end

This project has two parts:

  1. An interactive UI built with React
  2. A sophisticated parsing tool built with Flask

So we’ll employ lots of JavaScript and a good amount of Python to make it happen.

I’ll begin with creating the React front-end which takes care of uploading and sending the PDF file.

The parsing and search of the uploaded PDF is carried out by Flask later on.

Let’s start with React 👇

Start page wireframe

Laying out the start page

We kick things off by setting the stage–the start page.

The start page, in a way, “activates” the app. It’s where you find the start action that will enable each consecutive action thereafter as an individual step.

Actions like uploading the PDF file and searching for a term will unfold in steps neatly laid out. It’s a visual breakdown of the logical processing of the PDF that serves as a nice guide to a user.

Start page layout

Upload file screen

The upload screen is where the magic begins. With the ability to reset and start fresh, users can drag and drop or choose a file for upload.

Upload file screen wireframe

But we don’t stop there–control parameters curtail file type, size, and quantity.

For PinkPanda PDF, I limit the file type to a single PDF file at a time. This means the file has to have a .pdf extension to be considered a PDF file.

Validate pdf file

While the built-in browser file upload allows only PDF files when specifying accept="application/pdf", we need to restrict the drag-and-drop component to PDF files.

This we can do with a simple logical check on the type property of the uploaded file.

// FileUploadProgress.js
useEffect(() => {
        if (file && file.type === 'application/pdf') {
            // Execute file upload
            getPercentage(file);
        } else {
            setIsError(true);
            setIcon(fileFail);
            setIconAction(del);
            setIsUploaded(false);
        }
    }, [file]);

Hint 👁️‍🗨️
I’m managing state in the Context Provider using useReducer!

I, also, add a size limit of 184MB (I’m generous) to avoid overly bulky PDFs that require hours of resources that aren’t free 😅

Watch the progress display as your file uploads, and if impatience strikes, cancel the upload in progress.

File upload progress

Style inspo:

Upload file drag and drop component

The objective is to create a custom file input so it does the following:

  1. provide an area to drag and drop
  2. provide the option to select from files
  3. show upload progress
  4. show size (like kb) uploaded and the remainder to be uploaded

To make 1 and 2 from above possible, I’ll make an invisible input moved off-screen horizontally and vertically.

.dragDrop_file {
    /* hide from view for accessibility */
    position: absolute;
    left: -9999px;  
    top: -9999px;
    opacity: 0;
}

This CSS code will effectively hide the file input from view while ensuring it remains accessible to assistive technologies like screen readers and can be triggered using keyboard navigation.

Keep in mind that while the file input is hidden visually, it can still be interacted with programmatically and accessed via JavaScript or user interactions, such as clicking an associated label. This ensures that the file input remains usable and accessible to all users, including those who rely on assistive technologies.

Upload file screen

For the drag and drop area, use pointer-events to bypass the stopped propagation of the event listeners from parent to child.

Otherwise, the custom styles won’t apply when dragging over an area occupied by a child element.

Then for the upload progress, I’ll create a dynamic progress ring over the icons.

<svg
                    className="progress-ring"
                    width="50"
                    height="50">
                    <circle
                        stroke={isError ? 'red' : '#e0e0e0'}
                        strokeWidth="2"
                        fill="transparent"
                        r="23"
                        cx="25"
                        cy="25"
                        vectorEffect="non-scaling-stroke"
                        shapeRendering="geometricPrecision"
                        strokeLinecap="round" />
                    <circle
                        className={s['progress-ring__circle']}
                        strokeDasharray={`${23 * 2 * Math.PI} ${23 * 2 * Math.PI}`}
                        strokeDashoffset={`${(23 * 2 * Math.PI) - (percentage / 100) * (23 * 2 * Math.PI)}`}
                        stroke={isError ? 'red' : '#000'}
                        strokeWidth="3"
                        fill="transparent"
                        transform="rotate(-90 25 25)"
                        r="23"
                        cx="25"
                        cy="25"
                        vectorEffect="non-scaling-stroke"
                        shapeRendering="geometricPrecision"
                        strokeLinecap="round" />
                </svg>

Hey! Learn how to create your own SVG icons in How To Make Fire Custom SVG Icons With Adobe Illustrator.

Select an uploaded file

Once your file is securely uploaded, you can click to inspect it. But beware, error notifications await if something goes awry.

A fail icon and an error banner will guide you through any missteps. It’s all about keeping the user informed and in control.

Style inspo: Inventory Design System by K&Z Design

When working with form data, to inspect the form data object use the get() method with the key we assigned earlier, 'file' (it’s not creative, but it’s descriptive).

This will get an object with properties specific to the uploaded file.

// Example
lastModified : 1668218393995
lastModifiedDate: Fri Nov 11 2022 17:59:53 GMT-0800 (Pacific Standard Time)
name: "Git cheat sheet guide.pdf"
size: 136336
type: "application/pdf"
webkitRelativePath: ""

The next step is to send the file data to the server along with the search term 👇👇

Search PDF Screen

The pièce de résistance–the search screen. Here, a user can initiate a search or go back to the upload step with a New Upload button.

Search PDF screen

Error messages check if users forget to enter a search term or if the search yields no results.

Hey!
Error messages are very important. And I mean very important. It’s one of the best ways to communicate with users the progress, or possible lack thereof, so they don’t bounce off thinking something is “broken”. Be reasonably generous when communicating with your intended users 💁‍♀️

The response is a list of match items complete with images of the PDF page, page numbers, and truncated text.

But it doesn’t stop there. A skeleton loading animation adds a touch of flair while the search is underway.

Our skeleton component is versatile, allowing customization of size, the number of items within a wrapper, and the number of wrappers.

Related: The Magic Behind Skeleton Loading Animations

Scroll into view and interact with the response, where images are not just static–they’re interactive.

I create a scroll into view ref. Then, by placing ref.current.scrollIntoView inside a useEffect, you are ensuring that it’s called after the render and any updates to the refs have occurred. This is especially relevant when dealing with asynchronous operations or updates that might affect the layout or content of the component.

// ResultsList.js
let listRef = useRef(null);

    useEffect(() => {
        listRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [items]);

Tip 🔥
You don’t need to add the ref pointer as a dependency!

Displaying the search result

Now, for each item on the results list, you can zoom and pan to let you explore the details of each match.

Search results screen wireframe

To achieve the zoom and pan action, I’ll be using mouse events.

The onMouseMoveCapture event in React is similar to the regular onMouseMove event, but with an important difference: it is fired during the capture phase of the event propagation.

In the event propagation process, there are two main phases: the capture phase and the bubbling phase.

More on capture phases: This Is How To Build Custom Pagination With JavaScript

So, when you use onMouseMoveCapture, the event is triggered during the capture phase, before reaching the target element. This allows you to intercept the event before it reaches the specific component where the event occurred.

In the context of our code, onMouseMoveCapture is used to set the clickThreshold to zero when the mouse is moved. This is intended to capture the mouse movement even when the user drags the image.

By resetting the clickThreshold to zero during dragging, you’re essentially indicating that a recent drag event occurred, and the subsequent click should not trigger a zoom-out.

Honestly, everything here is to manage the pan and zoom functionality. Else you’ll be zooming out when intending to pan and vice versa 🙈

// Popup.js
const handleZoomToggle = (e) => {
        if (!isDragging && clickThreshold !== 0) {
            setZoom(!zoom);
            setPosition({ x: 0, y: 0 });
        } else {
            setClickThreshold(1);
        }
    }

const handleMouseMove = () => {
        if (isDragging) {
            setClickThreshold(0);
        }
    }

Lastly, click to enlarge the item in a popup, dismiss the modal, and select another match.

Moving on to the Flask backend

Time to peek behind the curtains at the Flask-powered back end pulling the strings.

Logical flow of file parsing and search

This is my first Flask application born out of necessity (apparently) so I’m exploring a lot of things along with you.

Hint 👀
It should go without saying but I’m gonna say it anyways for skimmers (like my dear self): You need Python 🐍 for this part.

To avoid overwhelming anyone who isn’t a Python expert (basically someone like me), I’ll attempt to break down the flow:

Uploaded file and search term journey:

The uploaded file and search term make their way from the React front-end to the Flask back-end for processing. It’s like passing the baton in a coding relay race.

// SearchForm.js
const formData = new FormData();
            formData.append('searchPhrase', searchPhrase);
            // formData.append('pdfPath', fileInputRef.current.files[0]);
            formData.append('pdfPath', uploadCtx.file);

            // let pdfPath = fileInputRef.current.files[0];

            const response = await axios.post(apiURL, formData, {
                headers: {
                    'Content-Type': 'multipart/form-data',
                }
            });

Handling CORS and storing assets:

Configure the Flask server to ensure proper handling of CORS. If serving the React app on a different port or domain than the Flask server, you might need to enable CORS on the Flask side to allow requests from the React app’s domain.

I use the flask-cors extension to do this, defining my allowed origins like so: CORS(app, origins=["<https://thehelpfultipper.github.io>", "<https://thehelpfultipper.github.io/pinkpanda_pdf>"])

Tip 💡
You might need to adjust certain env variables and configurations like CORS depending on the environment (i.e. production vs development).

Text extraction with pytesseract:

I’ll use pytesseract to extract text from the PDF page images.

Confession: it wasn’t easy to work with this package for a newbie. There are certain configurations to be put in place like specifying a tessdata variable and tesseract path both in dev and prod environments.

Tip 🔥
If installing tesseract with homebrew on MacOS, run brew ls tesseract to identify the paths of those directories since you do need to set them. Keep in mind though that the local tesseract directory path can be different from that on the deployed platform!

That said, it’s a good development skill to learn something new with each project 🥲

Defining routes:

I’ll define two routes–one for viewing images and another for handling post requests from the front end. It’s all about setting up the lanes for smooth communication.

Parsing, processing, and matching:

The real action begins. Data from the front end is parsed to get the PDF path and query term. The assets folder, acting as a results storage facility, resets with each new request.

OCR handling for special cases:

Text extraction isn’t foolproof. Handle special cases where text extraction is unsuccessful by parsing with OCR (Optical Character Recognition) in process_page_with_ocr. Split the text into words and process each word to calculate similarity score based on a threshold.

                    search_words = search_phrase.lower().split()
                    min_match_threshold = 80
                    for i in range(len(words) - len(search_words) + 1):
                        word_subset = words[i:i+len(search_words)]
                        combined_word = ' '.join(word_subset)
                        
                        if fuzz.partial_ratio(search_phrase, combined_word) >= min_match_threshold and \
           fuzz.partial_ratio(' '.join(search_words), combined_word) >= min_match_threshold:
                            match_found = True
                            break

Tip: Adjust the thresholds based on your needs. A higher threshold gives priority to exact matches, but keep in mind that using a lot of partial matches may require more processing power and result in longer load times for your app.

Maybe I’m being a bit extra leveling up this way but it’s me we’re talking about here 😅

Generating screenshots for matches:

For each match, generate a screenshot to save in assets storage. Extract the text and try to outline it in red. It’s not easy; we might not always be successful in outlining matched content since readability depends on PDF quality. Blurry or hard-to-read images require special handling.

The goal with all this? To create a visual index that enhances the user experience.

Deploying a Flask-React app

Deploying a Flask-React app involves a multi-step process.

I’ll be deploying the React front-end to GitHub Pages which is primarily designed for static websites.

For the Flask back-end, I’ll use render.com to deploy a python app as an alternative to Heroku (which these days requires a credit card even for free use).

Before starting, make sure the front-end and back-end are in separate directories within the project.

Tip: Have a clear distinction between your client-side code (React) and server-side code (Flask)!

Deploy React front-end to GitHub pages

I’ll use the gh-pages package to deploy the React front-end to GitHub pages.

We’ve done this before quite a few times on projects so it’s a simple matter.

After installing and configuring the package, all we need to do in the React frontend directory is run npm run deploy. This command will generate a build folder containing static HTML, CSS, and JavaScript files and deploy it through its corresponding repo.

APP: https://thehelpfultipper.github.io/pinkpanda_pdf/

How to deploy a Flask app to Render

I’ll be honest, deploying the Flask back-end was not a simple feat mainly because Tesseract is a total pain to deploy.

I never thought a day would come when I’d hate to see my error message as much as during this deployment process 😔

Note 🧐
The rest assumes you have created a Render account and you’re ready to create a deploy container!

Let’s set up a local .env file in the Flask app and reference those environment variables. There are two steps to this process:

Setting up local .env file

  1. Install python-dotenv: Run pip3 install in your terminal to install the python-dotenv package.
  2. Create .env file: In the Flask project directory, create a file named .env. Add your environment variables to this file in the format KEY=VALUE.
  3. Load .env in the Flask app: In the Flask app file, add the following lines at the top to load the .env file: from dotenv import load_dotenv load_dotenv().
  4. Accessing variables: You can now access the variables using os.getenv(). Don’t forget to manage your environment variables when deploying the app to a hosting platform.

Configure environment variables on Render

  1. Access the Render dashboard: Log in to your Render account and go to the Render dashboard.
  2. Select the web service: Navigate to the specific web service associated with the Flask app (if not already selected).
  3. Go to Environment: In the web service settings, find the “Environment” tab.
  4. Add environment variables: Add each environment variable used in the Flask app along with its corresponding value.
  5. Access environment variables in Flask: In the Flask app, use the os.getenv function to access the environment variables. Render will automatically set these variables for you during deployment.

Tip: Make sure that the environment variables set in Render match the keys you are using in your Flask app!

If the values of the env variables vary depending on the environment, I suggest designating an env variable to act as a flag. Mine is the boolean RENDER.

Deploying Flask back-end to Render

I’ll deploy with gunicorn to create the appropriate production environment in a venv.

Tip 👇
Render connects to your GitHub account for direct access to the app repo that you’re wanting to deploy.

I found that this deployment requires using a dockerized container to properly install Tesseract (since it’s not supported natively in Render) with the correct Dockerfile setup.

Don’t let something like Docker intimidate you, it’s possible to use even if you don’t have prior Docker experience. I found it more intuitive than the GitHub action workflows!

But I’m not going to lie and say deploying was straightforward. Since I’m not familiar with Render or Tesseract, it took days for me to figure out the correct setup and execution 😮‍💨

The absolute best thing you can do is keep this in mind: try, except are your best friends when you’re stuck and can’t tell WTH is going on.

Don’t shy away from using other util functions like traceback and shutil when trying to identify the Tesseract executable path.

And if you can deploy with Tesseract easily, all the power to you ⭐️

Mobile performance and optimization

Now, let’s talk about making sure your app slays on mobile.

Flask back-end’s final act

Implement some server-side caching to keep things snappy. Optimize that journey for mobile users because nobody’s got time for slow apps.

I use flask_caching to set the cache = Cache(app, config={'CACHE_TYPE': 'simple'}).

Then I hash the uploaded PDF content to check the cache for existing results and avoid duplicate efforts that demand additional processing power.

React front-end’s touch of practicality

Zoom, pan, touch—the trio of delight for mobile users. Enable touch navigation pan on zoom with touch events, and give them detailed user info messages for those hefty PDFs.

For more touch events and mobile swiping check out How To Make Your Pokémon API Project Interactive.

const getFetchMessage = () => {
        let msgIndex = 0;
        let lastUpdateTime = Date.now();
        const messages = ["Fetching data...", "Still fetching data.", "Hang in there! Fetching..."];
        const longMessages = ["Searching for your term–it's like finding a WiFi signal in the desert, but we're getting warmer!", "Your term is playing hide and seek–we're sharpening our seeker skills!", "...we're rolling out the red carpet, fashionably slow!", "Your term is just around the corner!"];
        const longAF = ["This PDF is longer than a Sunday afternoon nap, but fear not, we're keeping our eyes wide open.", "Your PDF is in marathon mode, but we've got our running shoes on...", "The PDF is unfolding like an epic saga...we're scripting the finale–stay tuned!", "PDF search in progress–your term is on the VIP list, enjoying the wait with a front-row seat!"];

        let cummTime = 0;
        const intervalId = setInterval( () => {
            const currentTime = Date.now();
            const ellapsedTime = (currentTime + cummTime) - lastUpdateTime;
            
            if(ellapsedTime >= 60000) {
                setFetchMessage([...longAF, ...messages][msgIndex]);
            }
            else if(ellapsedTime >= 30000) {
                setFetchMessage([...longMessages, ...messages][msgIndex]);
            } 
            else {
                setFetchMessage(messages[msgIndex]);
            } 

            msgIndex = (msgIndex + 1) % messages.length;
            lastUpdateTime = currentTime;
            cummTime += 5000;
        }, 5000);

        return intervalId;
    }

Keep them in the loop, entertained, and loving your app.

From concept to creation, it’s a wrap

And there you have it, THT-ears! We’ve walked through the creation of PinkPanda PDF, a custom file input that goes above and beyond.

From the sleek React front end to the brainy Flask back end, every step contributes to a seamless and engaging user experience.

So, grab your coding capes and dive into the world of PinkPanda PDF.

Build, experiment, and let your creativity flow. Until next time, keep coding like a pro 😁

Related Posts