Series: Building a Social Network with Flask & Stream – Part 12

Spencer P.
Spencer P.
Published March 27, 2020 Updated September 21, 2021

This article is the first installment of a tutorial series focused on how to create a full-stack application using Flask, React/Redux and Stream. This tutorial is an adaptation of my previous series on creating a Stream-based web app with Flask, so be sure to check it out to understand some of the structure behind it if you haven't already. In this piece, we set up the basic structure of our application with Server Side Rendering (SSR). Be sure to check out the repo to follow along!

This article assumes that you have npm installed and ready in your development environment, as well as a new directory initialized with a virtual environment.

Getting Started

We use SSR for our React application for a few key reasons. First, rather than splitting our code into two separate repositories simultaneously for server and client, we can combine it into one. Second, it allows us to avoid bothersome CORS-related issues while we develop by having all requests come from the same domain. Third, it gives us leeway to use some familiar packages (like flask-login) for authentication purposes, sparing us having to rewrite our functions and endpoints to use JWT instead.

Bare Bones

Our first step is to set up the basic skeleton of our project. Once we have the bones in place, we can start fleshing out specific aspects on which we are working. Similarly to our last project, we start with an empty directory initialized with a new virtual environment. Next, we install Flask itself along with a few base packages with pip install flask flask-sqlalchemy flask-login flask-wtf stream-python flask-migrate

From The Top

First up in the structure is to create our top-level Python files and directories. As this project is very similar on the backend to the previous series, we port most of the backend code directly over to save time and to avoid repeating familiar concepts. Be aware that there are differences between the two codebases as we go along.

We start by defining our application.py file (in application.py).

import os
from flask_migrate import Migrate, upgrade
from app import create_app, db
from app.models import User, Role, Permission, Content, Collection

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)


@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role,
                Permission=Permission, Content=Content,
                Collection=Collection)


@app.cli.command()
def test():
    """Run the unit tests."""
    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)


@app.cli.command()
def deploy():
    """Run deployment tasks."""
    # migrate database to latest revision
    upgrade()

    # create or update user roles
    Role.insert_roles()

    # ensure all users are following themselves
    User.add_self_follows()

    # ensure all users are following their collections
    User.add_self_collection_follows()

Config

After that, we set up our configuration file. This step includes best practices in dividing up our Stream keys as well as preparing to use AWS SES for emails copied from the end of the last tutorial (in config.py)

import os

basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY', 'secret')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_RECORD_QUERIES = True
    OFFBRAND_RESULTS_PER_PAGE = 15
    AWS_REGION_NAME = ''
    AWS_ACCESS_KEY_ID = ''
    AWS_SECRET_ACCESS_KEY = ''

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    STREAM_API_KEY = ''#Your API key here
    STREAM_SECRET = ''#Your Secret here
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    STREAM_API_KEY = ''#Your API key here
    STREAM_SECRET = ''#Your Secret here
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'
    WTF_CSRF_ENABLED = False


class ProductionConfig(Config):
    STREAM_API_KEY = ''#Your API key here
    STREAM_SECRET = ''#Your Secret here
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')

    @classmethod
    def init_app(cls, app):
        Config.init_app(app)


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,

    'default': DevelopmentConfig
}

App Directory

In continuation, we create our app directory that houses the majority of our application logic, along with an initialization file (in 'app/init.py')

from flask import Flask
from .extensions import db, login_manager
from config import config


def create_app(config_name):
    app = Flask(__name__, static_folder='./static/dist', template_folder='./static')
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    db.init_app(app)
    login_manager.init_app(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

You notice that our Flask initialization has new arguments for static and template folders. We have not used these arguments yet, but this helps Flask to understand from where we are serving our React App, and where to search for the static files that we create.

And By Extension

Next up is our extensions (in app/extensions.py)

from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

db = SQLAlchemy()

login_manager = LoginManager()

Re-Model

Considering that we have initialized Flask login, the application is expecting a user_loader function, even though we aren't directly asking for credentials. As we already defined all of our model objects from our previous project, we can directly transfer over that file (in app/models.py)

from datetime import datetime
import hashlib
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from flask_login import UserMixin, AnonymousUserMixin
from . import db, login_manager
import stream


class Permission:
    FOLLOW = 1
    COMMENT = 2
    WRITE = 4
    MODERATE = 8
    ADMIN = 16


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role', lazy='dynamic')

    def __init__(self, **kwargs):
        super(Role, self).__init__(**kwargs)
        if self.permissions is None:
            self.permissions = 0

    @staticmethod
    def insert_roles():
        roles = {
            'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
            'Moderator': [Permission.FOLLOW, Permission.COMMENT,
                          Permission.WRITE, Permission.MODERATE],
            'Administrator': [Permission.FOLLOW, Permission.COMMENT,
                              Permission.WRITE, Permission.MODERATE,
                              Permission.ADMIN],
        }
        default_role = 'User'
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.reset_permissions()
            for perm in roles[r]:
                role.add_permission(perm)
            role.default = (role.name == default_role)
            db.session.add(role)
        db.session.commit()

    def add_permission(self, perm):
        if not self.has_permission(perm):
            self.permissions += perm

    def remove_permission(self, perm):
        if self.has_permission(perm):
            self.permissions -= perm

    def reset_permissions(self):
        self.permissions = 0

    def has_permission(self, perm):
        return self.permissions & perm == perm

    def __repr__(self):
        return '<Role %r>' % self.name


class CollectionFollow(db.Model):
    __tablename__ = 'collection_follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    collection_id = db.Column(db.Integer, db.ForeignKey('collections.id'),
                              primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)


class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)


class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    password_hash = db.Column(db.String(128))
    confirmed = db.Column(db.Boolean, default=False)
    name = db.Column(db.String(64))
    about_me = db.Column(db.Text())
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
    avatar_hash = db.Column(db.String(32))
    collections = db.relationship('Collection', backref='author', lazy='dynamic', cascade='all, delete-orphan')
    followed = db.relationship('Follow',
                               foreign_keys=[Follow.follower_id],
                               backref=db.backref('follower', lazy='joined'),
                               lazy='dynamic',
                               cascade='all, delete-orphan')
    followers = db.relationship('Follow',
                                foreign_keys=[Follow.followed_id],
                                backref=db.backref('followed', lazy='joined'),
                                lazy='dynamic',
                                cascade='all, delete-orphan')
    followed_collection = db.relationship('CollectionFollow',
                                          foreign_keys=[CollectionFollow.follower_id],
                                          backref=db.backref('c_follower', lazy='joined'),
                                          lazy='dynamic',
                                          cascade='all, delete-orphan')

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.role is None:
            if self.email == current_app.config['OFFBRAND_ADMIN']:
                self.role = Role.query.filter_by(name='Administrator').first()
            if self.role is None:
                self.role = Role.query.filter_by(default=True).first()
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = self.gravatar_hash()
        self.follow(self)

    @staticmethod
    def add_self_follows():
        for user in User.query.all():
            if not user.is_following(user):
                user.follow(user)
                db.session.add(user)
                db.session.commit()

    @staticmethod
    def add_self_collection_follows():
        for user in User.query.all():
            for collection in user.collections:
                if not user.is_following_collection(collection):
                    user.follow_collection(collection)
                    db.session.add(user)
                    db.session.commit()

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id}).decode('utf-8')

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        try:
            # Attempt to add user to Stream
            client = stream.connect(current_app.config["STREAM_API_KEY"], current_app.config['STREAM_SECRET'])
            client.users.add(str(self.id), {"username": self.username, "gravatar": self.gravatar(size=40)})
        except:
            # If attempt is unsuccessful, return False
            return False
        self.confirmed = True
        db.session.add(self)
        return True

    def generate_reset_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'reset': self.id}).decode('utf-8')

    @staticmethod
    def reset_password(token, new_password):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        user = User.query.get(data.get('reset'))
        if user is None:
            return False
        user.password = new_password
        db.session.add(user)
        return True

    def generate_email_change_token(self, new_email, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps(
            {'change_email': self.id, 'new_email': new_email}).decode('utf-8')

    def change_email(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('change_email') != self.id:
            return False
        new_email = data.get('new_email')
        if new_email is None:
            return False
        if self.query.filter_by(email=new_email).first() is not None:
            return False
        self.email = new_email
        self.avatar_hash = self.gravatar_hash()
        db.session.add(self)
        try:
            # Attempt to add edited user to Stream
            client = stream.connect(current_app.config["STREAM_API_KEY"], current_app.config['STREAM_SECRET'])
            client.users.update(str(self.id), {"username": self.username, "gravatar": self.gravatar(size=40)})
        except:
            # If attempt is unsuccessful, return False
            return False
        return True

    def can(self, perm):
        return self.role is not None and self.role.has_permission(perm)

    def is_administrator(self):
        return self.can(Permission.ADMIN)

    def ping(self):
        self.last_seen = datetime.utcnow()
        db.session.add(self)

    def gravatar_hash(self):
        return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()

    def gravatar(self, size=100, default='identicon', rating='g'):
        url = 'https://secure.gravatar.com/avatar'
        hash = self.avatar_hash or self.gravatar_hash()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
            url=url, hash=hash, size=size, default=default, rating=rating)

    def stream_user_token(self):
        client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
        return client.create_user_token(str(self.id))

    def follow(self, user):
        client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
        user_feed = client.feed("Notifications", str(self.id))
        if not self.is_following(user):
            user_feed.follow("User", str(user.id))
            f = Follow(follower=self, followed=user)
            db.session.add(f)
            return True

    def unfollow(self, user):
        client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
        user_feed = client.feed("Notifications", str(self.id))
        f = self.followed.filter_by(followed_id=user.id).first()
        if f:
            user_feed.unfollow("User", str(user.id))
            db.session.delete(f)
            return True

    def is_following(self, user):
        if user.id is None:
            return False
        return self.followed.filter_by(
            followed_id=user.id).first() is not None

    def is_followed_by(self, user):
        if user.id is None:
            return False
        return self.followers.filter_by(
            follower_id=user.id).first() is not None

    def is_following_collection(self, collection):
        if collection is None:
            return False
        if collection.id is None:
            return False
        return self.followed_collection.filter_by(
            collection_id=collection.id).first() is not None

    def follow_collection(self, collection):
        client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
        user_feed = client.feed("Timeline", str(self.id))
        if not self.is_following_collection(collection):
            user_feed.follow("Collections", str(collection.id))
            f = CollectionFollow(c_follower=self, following=collection)
            db.session.add(f)
            return True

    def unfollow_collection(self, collection):
        client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
        user_feed = client.feed("Timeline", str(self.id))
        f = self.followed_collection.filter_by(
            collection_id=collection.id).first()
        if f:
            user_feed.unfollow("Collections", str(collection.id))
            db.session.delete(f)
            return True

    def __repr__(self):
        return '<User %r>' % self.username


class AnonymousUser(AnonymousUserMixin):
    def can(self, permissions):
        return False

    def is_administrator(self):
        return False

    def stream_user_token(self):
        return None


login_manager.anonymous_user = AnonymousUser


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


class Collection(db.Model):
    __tablename__ = 'collections'
    id = db.Column(db.Integer, primary_key=True)
    stream_id = db.Column(db.String(64))
    name = db.Column(db.String(64))
    description = db.Column(db.String(256))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    content = db.relationship('Content', backref='collection', lazy='dynamic', cascade='all, delete-orphan')
    user_followers = db.relationship('CollectionFollow',
                                     foreign_keys=[CollectionFollow.collection_id],
                                     backref=db.backref('following', lazy='joined'),
                                     lazy='dynamic',
                                     cascade='all, delete-orphan')

    def has_content(self, url):
        return self.content.filter_by(url=url).first() is not None

    def add_to_stream(self):
        # Initialize client and add activity to user feed
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.feed("User", str(self.author_id)).add_activity({'actor': client.users.create_reference(str(self.author_id)),
                                                                   'verb': 'create',
                                                                   'object': 'Collection:' + str(self.id),
                                                                   'create': self.name,
                                                                   'foreign_id': 'Collection:' + str(self.id),
                                                                   'description': self.description,
                                                                   'time': self.timestamp
                                                                   })
            return True
        except:
            # If the Stream Client throws an exception or there is a network issue
            return False

    def update_stream(self):
        # Initialize client and update activity
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.update_activity({'actor': client.users.create_reference(str(self.author_id)),
                                    'verb': 'create',
                                    'object': 'Collection:' + str(self.id),
                                    'create': self.name,
                                    'foreign_id': 'Collection:' + str(self.id),
                                    'description': self.description,
                                    'time': self.timestamp
                                    })
            return True
        except:
            return False

    def delete_from_stream(self):
        # Initialize client and delete activity
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            user_feed = client.feed('User', str(self.author_id))
            user_feed.remove_activity(foreign_id="Collection:" + str(self.id))
            return True
        except:
            return False


class Content(db.Model):
    __tablename__ = 'content'
    id = db.Column(db.Integer, primary_key=True)
    stream_id = db.Column(db.String(64))
    image = db.Column(db.String, default=None)
    title = db.Column(db.String(64))
    url = db.Column(db.String(128))
    description = db.Column(db.String(256))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    collection_id = db.Column(db.Integer, db.ForeignKey('collections.id'))

    def add_to_stream(self):
        # Initialize client and add activity to user feed
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.feed("Collections", str(self.collection_id)).add_activity({'actor': client.users.create_reference(str(self.collection.author_id)),
                                                                              'verb': 'post',
                                                                              'object': 'Content:' + str(self.id),
                                                                              'post': self.title,
                                                                              'url': self.url,
                                                                              'image': self.image,
                                                                              'description': self.description,
                                                                              'time': self.timestamp,
                                                                              'collection': {
                                                                                  'id': self.collection_id,
                                                                                  'name': self.collection.name
                                                                                   },
                                                                              'foreign_id': 'Content:' + str(self.id)
                                                                             })
            return True
        except:
            # If the Stream Client throws an exception or there is a network issue
            return False

    def add_fields_to_stream(self, **kwargs):
        # Update Stream with new/removed fields
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.activity_partial_update(foreign_id='Content:' + str(self.id),
                                           time=self.timestamp,
                                           set=kwargs
                                           )
            return True
        except:
            return False

    def remove_fields_from_stream(self, **kwargs):
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.activity_partial_update(foreign_id='Content:' + str(self.id),
                                           time=self.timestamp,
                                           unset=kwargs
                                           )
            return True
        except:
            return False

    def update_stream(self):
        # Initialize client and update activity
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            client.update_activity({'actor': client.users.create_reference(str(self.collection.author_id)),
                                    'verb': 'post',
                                    'object': 'Content:' + str(self.id),
                                    'post': self.title,
                                    'url': self.url,
                                    'description': self.description,
                                    'time': self.timestamp,
                                    'collection': {'id': self.collection_id,
                                                   'name': self.collection.name},
                                    'foreign_id': 'Content:' + str(self.id)
                                    })
            return True
        except:
            return False

    def delete_from_stream(self):
        # Initialize client and delete activity
        try:
            client = stream.connect(current_app.config['STREAM_API_KEY'], current_app.config['STREAM_SECRET'])
            user_feed = client.feed('Collections', str(self.collection_id))
            user_feed.remove_activity(foreign_id="Content:" + str(self.id))
            return True
        except:
            return False

Migration Habits

We must initialize the database and create the tables to allow for Flask-login as well, so we can do that now as well with flask db init, flask db migrate, and flask db upgrade in the CLI.

Just For Decoration

To speed up our development, we transfer over our route decorators that determine permission levels for users (in app/decorators.py).

from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission


def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator


def admin_required(f):
    return permission_required(Permission.ADMIN)(f)

Email Me

Along the same lines of logic outlined above, we include the email function that we developed earlier as well (in app/email.py).

from flask import current_app, render_template
import boto3
from botocore.exceptions import ClientError


def send_email(to, subject, template, **kwargs):
    """Function to send email for user confirmations/updates, uses
    a threading model to not offload the processing from the main
    thread"""

    client = boto3.client('ses', region_name=current_app.config['AWS_REGION_NAME'],
                          aws_access_key_id=current_app.config['AWS_ACCESS_KEY_ID'],
                          aws_secret_access_key=current_app.config['AWS_SECRET_ACCESS_KEY'])

    try:
        response = client.send_email(
            Destination={
                'ToAddresses': [
                    to,
                ],
            },
            Message={
                'Body': {
                    'Html': {
                        'Charset': 'UTF-8',
                        'Data': render_template(template + '.html', **kwargs),
                    },
                    'Text': {
                        'Charset': 'UTF-8',
                        'Data': render_template(template + '.txt', **kwargs),
                    },
                },
                'Subject': {
                    'Charset': 'UTF-8',
                    'Data': subject,
                },
            },
            Source=''# Your Amazon Registered Address,
        )
    except ClientError as e:
        print(e.response['Error']['Message'])
    else:
        print("Email sent! Message ID:")
        print(response['MessageId'])
    return True

Main Event

Next, we create our main directory where we host our (you guessed it!) main application routes. Once again, we must create an initialization file for it (in app/main/__init__.py)

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views

First View

Now that our main directory is built with an initialization file, we can move onto our views. To start, we only have one view to return the index.html file that we create, so we need to set that up (in app/main/views.py)

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
from . import main
from flask import render_template


@main.route('/')
def index():
    return render_template("index.html")

Next Steps

Now that the backend is created, we can move on to building the React application that returns. This part is a bit more hands-on than a simple 'create-react-app', but it is still very straight-forward.

In our app directory, create a new directory named 'static', followed by subdirectories' css', 'dist', 'images' and 'js'. These elements help to keep our project organized between different components (in a broad sense) as we continue to develop. Now, using the command line, navigate to the static folder using cd app/static.

Finally, initialize the project with npm init. We are guided through our configuration for the newly created package.json, which I have used the following values for:

Webpacking

After you have created your package.json file, our next step is to install webpack from the command line, en npm i webpack --save-dev

Once we install this element, create a webpack configuration file in your static directory (in 'app/static/webpack.config.js')

const webpack = require('webpack');
const config = {
    entry:  __dirname + '/js/index.jsx',
    output: {
        path: __dirname + '/dist',
        filename: 'bundle.js',
    },
    resolve: {
        extensions: ['.js', '.jsx', '.css']
    },
};
module.exports = config;

Babelling

Now, we need to install babel to convert the jsx files. In the command line, install babel with npm i @babel/preset-env @babel/preset-react --save-dev.

We also need to create a babel presets file to handle conversions (in app/static/.babelrc)

"presets": [
  "@babel/preset-env",
  "@babel/preset-react"
}

After, we have to add a babel-loader rule to the webpack config, which excludes any files node modules to speed up our loading times (in app\static\webpack.config.js)

module: {
  rules: [
    {
      test: /\.jsx?/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
  ]
}

Home Stretch

Our last steps include creating our index files, which are the base of our React web application. These files consist of the index.html file, which is rendered by our "view" function to a user requesting our site. The second component is a index.jsx file, which contains the React logic rendered to the page.

First, we create the index.html file (in 'app/static/index.html')

<!— index.html —>
<html>
  <head>
    <meta charset="utf-8">
    <title>Offbrand React/Redux And Stream Tutorial</title>
  </head>
  <body>
    <div id="root" />
    <script src="dist/bundle.js" type="text/javascript"></script>
  </body>
</html>

Followed by the index.jsx file (in ‘app/static/js/index.jsx’)

import ReactDOM from 'react-dom'
import React from 'react'

ReactDOM.render((
    <h1>Offbrand React/Redux Tutorial With Stream!</h1>
), document.getElementById('root'));

Building The Builders

Our final step is to create our build files within the package.json file to update our dependencies.

{
  "name": "offbrand-react-tutorial",
  "version": "1.0.0",
  "description": "A Tutorial to build a web app using Flask, React/Redux, and Stream",
  "main": "index.js",
  "scripts": {
    "build": "webpack -p --progress --config webpack.config.js",
    "dev-build": "webpack --progress -d --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "webpack --progress -d --config webpack.config.js --watch"
  },
  "author": "Spencer Porter",
  "license": "ISC",
  "devDependencies": {
    "@babel/preset-env": "^7.9.0",
    "@babel/preset-react": "^7.9.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "@babel/core": "^7.9.0",
    "babel": "^6.23.0",
    "babel-loader": "^8.1.0"
  },
  "proxy": "http://localhost:5000"
}

(I have included a proxy to avoid CORS-related issues that might spring up along the way from opening files in an unorthodox manner.)

Before moving on, we need to generate our first build of the React application by running npm run watch. While there shouldn't be any dependency issues if you have followed the instructions up until to this point, if any problems arise during the build, run npm install and try again.

Sanity Test

Now, your application tree should look like this:

To check that everything is working correctly, open a new CLI or exit the build with CTRL+C and navigate back to the top level of Flask project. From there, set the application with set FLASK_APP=application.py in the command line and enter flask run. When we open up our browser to localhost (http://127.0.0.1:5000), our app should render!

As a helpful tip, be sure to clear your browser cache between new builds, as most web browsers store previous versions of your application, which can cause headaches while you develop.

Final Thoughts

At this point, we have the structure in place to start to develop your Full Stack Application. Now that we have set up these essential elements, we can begin to develop our authentication functions to verify users and create profiles. Look for these steps in the next tutorial!

As always, thanks for reading, and happy coding!

Note: The next post in this series can be found here

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->