/React-Tutorial

A summary of my attempts to learn react and integrate it with an existing Flask back-end

Primary LanguageHTML

Background

When building a large codebase from scratch, usually there's periods of growth interspersed with periods of consolidation. In my case, I was building a moderately-sized Flask project whose front-end consisted of many interwoven html templates and vanilla JS scripts. Due to the increasing size and complexity, I decided to invest in learning a framework, and React seemed like a good choice given its popularity and reputation. This repo consists of my attempts to learn React and to integrate it into an existing Flask project.

Learning React

In my experience, when confronting any new technology, a good "first step" is to walk through some of the tutorials in order to internalize the basics of that technology. As with learning any new skill, the first step was to google "learn react reddit" and click the first result. This quickly led me to the official docs at react.dev/learn.

Initial Friction

The official docs have a tic-tac-toe tutorial, which was my first real foray into actually using React. I became extremely frustrated with this tutorial at the time. This tutorial relies heavily on a magic "run everything in the browser" tool at codesandbox.io, but to me this felt "cutesy" for someone trying to learn a serious tool and integrate it into a real live-running project. There were instructions for how to run the tutorial locally:

React Installation Instructions

But following these instructions led to an error:

First Local Install Error

After some googling I changed App.js from this:

export default function Square() {
  return <button className="square">X</button>;
}

To this:

import React from "react";

export default function Square() {
  return <button className="square">X</button>;
}

Which compiled! But then the next step was to return two buttons:

export default function Square() {
    return (
        <>
            <button className="square">X</button>
            <button className="square">X</button>
        </>
    );
}

Which again failed:

Second Local Install Error

At this point all the google results were saying "you're using JS and need to be using JSX duhhh!!!" even though this is the default installation from their repository. I finally said "fuck it" and used the stupid cutesy in-browser tool for the rest of the tutorial. At some point even this broke and I had to re-fork the code from a later step to get it working again. Hopefully the devs at React will fix the local install issues. In hindsight the tutorial was quite good from a conceptual standpoint, but the install issues were very frustrating at the time.

React

To put it in my own words, the main high-level motivation behind React is the ability to develop a front-end by nesting custom html-like entities called components. Components are programmable, re-usable, and can even be passed variables from components higher up in the DOM tree. Components return an object that maps to an embeddable HTML fragment. Probably their most useful feature is the ability to seamlessly react to changes in state, hence the name React. Components can even be given callback functions that update state in their parent components, called hooks. Going by the tic-tac-toe example, a typical React project might have immutable state variables defined higher in the Component tree, with hooks that update these state variables lower in the Component tree. Note that "immutable" in this context refers to an existing value not changing state, however the state can always be updated to a new immutable value.

Flask Integration

One of the problems I ran into when searching for "Flask+React" was that most tutorials (eg ref1, ref2) assumed that the entire front-end would be served with a React-focused full-stack framework (such as node.js), while the Flask back-end would exist solely as a separate API returning JSON files from a database. While this final design would probably be better overall, my general approach to refactoring is to proceed as gradually as possible. Given I already have a codebase full of Flask templates and Javascript files, my preference would be to start carving out React components from the inside-out, starting with the most obvious use-cases first, until eventually maybe there's no more templates and it's one big React Component at the top, and then perhaps I would switch to npm for the front-end.

I found this really great Medium Tutorial that integrated React with Flask in three different ways, without relying on a separate node server. As I worked through the tutorial, I tried to serve up the tic-tac-toe example in its final form, embedded as a Component inside a Flask template.

Example 1 - CDN

The "easiest" (and worst) way to integrate React is with a "Content Delivery Network" or CDN. In the CDN approach, all the libraries for running React are fetched externally from the user's browser as the page is loading. There are a couple problems with this. First it could be slow, since the user has to make multiple requests to multiple different sites before the page starts working. Second, there are potential vulnerabilities (both security and reliability) by loading external scripts. For all we know, five or ten years from now the link might no longer even work.

That being said, it is the easiest to get up and running, since it requires no additional installations on the back-end side. Although "easiest" is perhaps a misnomer, as I still had to slog through quite a few errors before I was able to get tic-tac-toe to work. The folder 'react-flask-example/example-01-cdn' has a working final version, and can be run with these commands and navigating to localhost:5000:

cd react-flask-example
cd example-01-cdn
virtualenv venv
./venv/scripts/activate
pip install -r requirements.txt
flask run

It works!

Working Tic-Tac-Toe

Here's the code for the template:

<!DOCTYPE html>
<html>
    <head>
        <title>Hello World Example</title>
        <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='styles.css') }}">
        <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
        <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
        <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.22.9/babel.js"></script>
        <script>var exports={};</script>
        <script type="text/babel" src="{{url_for('static', filename='App.js')}}"></script>
        <script type="text/babel" src="{{url_for('static', filename='index.js')}}"></script>
    </head>
    <body>
        <p>Hello from Flask Template!</p>
        <div id="root">
        </div>
    </body>
</html>

The code for App.js needed to change this:

import { useState } from 'react';

To this:

const { useState } = React;

While the code for index.js was changed from this:

import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";

import App from "./App";

const root = createRoot(document.getElementById("root"));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

To this:

const { StrictMode } = React;
const { createRoot } = ReactDOM;

const root = createRoot(document.getElementById("root"));
root.render(
  <StrictMode>
    <Game />
  </StrictMode>
);

Main things to note are:

  1. The scripts for React and Babel are loaded in the first three script tags that have have the crossorigin property. Babel is a JS library that can take JSX scripts with the "text/babel" tag as input and transform them into a working JS script as output.
  2. The line var exports={}; is a bit peculiar. Without it the code doesn't work, and the browser console produces this error: Uncaught ReferenceError: exports is not defined. Searching this error brought me to this SO post, with the needed fix as the first response. A comment in that SO post links to this other SO post with a similar error require is not defined. Apparently it's because commands like export and require don't exist in default browser JS but do exist in a related language called CommonJS, which is used in many React compilation environments.
  3. Using import { x } from 'y'; causes the error x is not defined, and the fix to use const { x } = Y; was found from this SO post.
  4. Not sure how I discovered changing <App /> to <Game />, it might have been from reading the Mozilla Docs and figured the original name might be in the global scope instead of the filename. I'm still not 100% sure how the whole "export name = filename" works in a compiled environment such as npm.

In any case I probably spent more time on this step than I needed, but I learned a few things about Babel and Javascript imports/exports in the process so maybe it was worth it.

Example 2 - React CDN With Pre-Compiled JSX

The next step lifted the JSX compilation out of the browser using a locally installed version of Babel. Like the first example, my goal was to get the full tic-tac-toe example working, with the latest versions of everything. I did run into some frustrations (as usual), but eventually got it working. This example is in the folder called 'react-flask-example/example-02-compile-jsx'. To get it working, run these steps:

npm init -y
npm install @babel/core
npm install @babel/cli
npm install @babel/preset-react
npx babel src --out-dir static --presets @babel/preset-react
flask run --debug

The jsx files are located in src and compiled with npx babel into regular javascript, which are output into the static folder. There was a little bit of trickiness to figure out the correct install scripts, as the tic-tac-toe requires Babel 7 but the flask+react tutorial was using Babel 6. There was one update needed for App.js to work, mainly replacing:

export default function Game() {
    ...

With no export default:

function Game() {
    ...

With the export keyword it was complaining that App.js needed to be a module in order to export, but adding type="module" did nothing to fix the error. There might be a better way to define App.js as a module but for now it works and I'm moving on.

There were no changes to index.js.

There was one non-trivial change to index.html, which now looks like this:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Flask and React Project</title>
    <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='styles.css') }}">
</head>
<body>
    <p>Hello from Flask Template!</p>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="{{url_for('static', filename='App.js')}}"></script>
    <script src="{{url_for('static', filename='index.js')}}"></script>
</body>
</html>

Mainly the scripts were moved after the <div id="root"> tag. Putting them before results in this error:

Uncaught Error: createRoot(...): Target container is not a DOM element.

Searching this error resulted in this fix. Apparently putting the scripts in the <head> tag makes the React scripts run before the DOM has fully loaded, making it unable to find the <div id="root"> element. I didn't need to do this in the first example, but that might be because using a CDN for Babel made it wait for Babel to load, causing the React scripts to run after the DOM had fully loaded. In any case I'm not 100% sure, but it works and that's good enough for a throwaway example.