React Setup
Part 1, Chapter 4
Let's turn our attention to the client-side and add React.
React is a declarative, component-based, JavaScript library for building user interfaces.
If you're new to React, review the official tutorial and the excellent Why did we build React? blog post. You may also want to step through the Intro to React tutorial to learn more about Babel and Webpack -- and how they work behind the scenes.
Make sure you have Node and NPM installed before continuing:
$ node -v
v19.7.0
$ npm -v
9.5.0
Project Setup
We'll be using the amazing Create React App CLI to generate a boilerplate that's all set up and ready to go.
Again, it's important to understand what's happening under the hood with Webpack and Babel. For more, check out the Intro to React tutorial.
Add a new directory in "services" called "client", and then cd
into the newly created directory and create the boilerplate with npx:
$ npx [email protected] .
Along with creating the basic project structure, this will also install all dependencies. This will take several minutes. Welcome to modern JavaScript. Once done, start the server:
$ npm start
After starting the server, Create React App automatically launches the app in your default browser on http://localhost:3000.
Ensure all is well, and then kill the server. Now we're ready to build our first component!
First Component
First, to simplify the structure, remove every file and folder from the "services/client/src" folder except index.js and setupTests.js. Then, update index.js:
import { createRoot } from 'react-dom/client';
const App = () => {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1 is-1">Users</h1>
<hr/><br/>
</div>
</div>
</div>
</section>
)
};
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
What's happening?
- After importing
createRoot
, we created a functional component calledApp
, which returns JSX. -
We then used the
render
method to mount the App to the DOM to the HTML element with an ID ofroot
.Take note of
<div id="root"></div>
within the index.html file in the "public" folder.
Add Bulma to index.html (found in the "public" folder) in the head
:
<link
href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.9.3/css/bulma.min.css"
rel="stylesheet"
>
Start the server again to see the changes in the browser:
$ npm start
Class-based Component
Update index.js:
import { createRoot } from 'react-dom/client';
import { Component } from 'react'; // new
// new
class App extends Component {
constructor() {
super();
}
render() {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1">Users</h1>
<hr/><br/>
</div>
</div>
</div>
</section>
)
}
};
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
What's happening?
- We created a class-based component, which runs automatically when an instance is created (behind the scenes).
- When the app is run,
super()
calls the constructor ofComponent
, whichApp
extends from.
You may have already noticed, but the output in the browser is the exact same as before, despite using a class-based component. We'll look at the differences between the two shortly!
AJAX
To connect the client to the server, add a getUsers()
method to the App
class, which uses Axios to manage the AJAX call:
getUsers() {
axios.get(`${process.env.REACT_APP_API_SERVICE_URL}/users`)
.then((res) => { console.log(res); })
.catch((err) => { console.log(err); });
}
Install Axios:
$ npm install [email protected] --save
Add the import:
import axios from 'axios';
You should now have:
import { createRoot } from 'react-dom/client';
import { Component } from 'react';
import axios from 'axios'; // new
class App extends Component {
constructor() {
super();
}
// new
getUsers() {
axios.get(`${process.env.REACT_APP_API_SERVICE_URL}/users`)
.then((res) => { console.log(res); })
.catch((err) => { console.log(err); });
}
render() {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1">Users</h1>
<hr/><br/>
</div>
</div>
</div>
</section>
)
}
};
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
To connect this up to the api
service, open a new terminal window, navigate to the project root, and update the containers:
$ docker-compose up -d
Ensure the app is working in the browser, and then run the tests:
$ docker-compose exec api python -m pytest "src/tests" -p no:warnings
Now, turning back to React, we need to add the environment variable, process.env.REACT_APP_API_SERVICE_URL
. Kill the Create React App server (if it's running), and then run:
$ export REACT_APP_API_SERVICE_URL=http://localhost:5004
All custom environment variables must begin with
REACT_APP_
. For more, check out the official docs.
We still need to call the getUsers()
method, which we can call, for now, in the constructor()
:
constructor() {
super();
this.getUsers(); // new
}
Run the server (npm start
) and then within Chrome DevTools, open the JavaScript Console. You should see the following error:
Access to XMLHttpRequest at 'http://localhost:5004/users'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
In short, we're making a cross-origin AJAX request (from http://localhost:3000
to http://localhost:5004
), which is a violation of the browser's "same origin policy". Fortunately, we can use the Flask-CORS extension to handle this.
Within the "users" directory, add Flask-CORS to the requirements.txt file:
flask-cors==3.0.10
To keep things simple, let's allow cross origin requests on all routes, from any domain. Simply update services/users/src/__init__.py like so:
# src/__init__.py
import os
from flask import Flask
from flask_admin import Admin
from flask_cors import CORS # new
from flask_sqlalchemy import SQLAlchemy
from werkzeug.middleware.proxy_fix import ProxyFix
# instantiate the extensions
db = SQLAlchemy()
cors = CORS() # new
admin = Admin(template_mode="bootstrap3")
def create_app(script_info=None):
# instantiate the app
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
# set config
app_settings = os.getenv("APP_SETTINGS")
app.config.from_object(app_settings)
# set up extensions
db.init_app(app)
cors.init_app(app, resources={r"*": {"origins": "*"}}) # new
if os.getenv("FLASK_ENV") == "development":
admin.init_app(app)
# register api
from src.api import api
api.init_app(app)
# shell context for flask cli
@app.shell_context_processor
def ctx():
return {"app": app, "db": db}
return app
To test, start by updating the containers:
$ docker-compose up -d --build
Then, update and seed the database:
$ docker-compose exec api python manage.py recreate_db
$ docker-compose exec api python manage.py seed_db
With both apps running, navigate to http://localhost:3000, open the JavaScript Console again, and this time you should see the results of console.log(res);
:
Let's parse the JSON object:
getUsers() {
axios.get(`${process.env.REACT_APP_API_SERVICE_URL}/users`)
.then((res) => { console.log(res.data); }) // new
.catch((err) => { console.log(err); });
}
Now you should have an array with two objects in the JavaScript Console:
[
{
"created_date": "2020-11-14T14:16:18.033783",
"email": "[email protected]",
"id": 1,
"username": "michael"
},
{
"created_date": "2020-11-14T14:16:18.033783",
"email": "[email protected]",
"id": 2,
"username": "michaelherman"
}
]
Before we move on, we need to do a quick refactor. Remember how we called the getUsers()
method in the constructor?
constructor() {
super();
this.getUsers();
};
Well, the constructor()
fires before the component is mounted to the DOM. What would happen if the AJAX request took longer than expected and the component mounted before the request completed? This introduces a race condition. Fortunately, React makes it fairly simple to correct this via Lifecycle Methods.
Component Lifecycle Methods
Class-based components have several functions available that execute at certain times during the life of the component. These are called Lifecycle Methods. Take a quick look at the official documentation to learn about each method and when each is called.
The AJAX call should be made in the componentDidMount()
method:
componentDidMount() {
this.getUsers();
};
Update the component:
class App extends Component {
// updated
constructor() {
super();
};
// new
componentDidMount() {
this.getUsers();
};
getUsers() {
axios.get(`${process.env.REACT_APP_API_SERVICE_URL}/users`)
.then((res) => { console.log(res.data); })
.catch((err) => { console.log(err); });
}
render() {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1">Users</h1>
<hr/><br/>
</div>
</div>
</div>
</section>
)
}
};
Make sure everything still works as it did before.
State
To add the state -- i.e., the users -- to the component we need to use setState()
, which is an asynchronous function used to update state.
Update getUsers()
:
getUsers() {
axios.get(`${process.env.REACT_APP_API_SERVICE_URL}/users`)
.then((res) => { this.setState({ users: res.data }); }) // updated
.catch((err) => { console.log(err); });
};
Add state to the constructor:
constructor() {
super();
// new
this.state = {
users: []
};
};
So, this.state
adds the state property
to the class and sets users
to an empty array.
Review Using State Correctly from the official docs.
Finally, update the render()
method to display the data returned from the AJAX call to the end user:
render() {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1">Users</h1>
<hr/><br/>
{/* new */}
{
this.state.users.map((user) => {
return (
<p
key={user.id}
className="box title is-4 username"
>{ user.username }
</p>
)
})
}
</div>
</div>
</div>
</section>
)
}
What's happening?
- We iterated over the users (from the AJAX request) and created a new
<p>
element for each user. This is why we needed to set an initial state of an empty array -- it preventsmap
from exploding. key
is used by React to keep track of each element. Review the official docs for more.
Functional Component
Let's create a new component for the users list. Add a new folder called "components" to "services/client/src". Add a new file to that folder called UsersList.jsx:
import React from 'react';
const UsersList = (props) => {
return (
<div>
{
props.users.map((user) => {
return (
<p
key={user.id}
className="box title is-4 username"
>{ user.username }
</p>
)
})
}
</div>
)
};
export default UsersList;
Why did we use a stateless functional component here rather than a stateful class-based component?
Notice how we used props
instead of state
in this component. Essentially, you can pass state to a component with either props
or state
:
- Props: data flows down via
props
(fromstate
toprops
), read only - State: data is tied to a component, read and write
For more, check out ReactJS: Props vs. State.
It's a good practice to limit the number of stateful class-based components since they can manipulate state and are, thus, less predictable. If you just need to render data (like in the above case), then use a stateless functional component.
Now we need to pass state from the parent to the child component via props
.
First, add the import to index.js:
import UsersList from './components/UsersList';
Then, update the render()
method:
render() {
return (
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-one-third">
<br/>
<h1 className="title is-1">Users</h1>
<hr/><br/>
<UsersList users={this.state.users}/>
</div>
</div>
</div>
</section>
)
}
Review the code in each component and add comments as necessary.
Make sure your app looks like this in the browser:
✓ Mark as Completed