•over 1 year ago
This article is the second installment of a tutorial series focused on how to create a full-stack application using Flask, React/Redux and Stream. In this piece, we are going to be starting our authentication flow with registration and user confirmation, as well as getting up and running with React-Redux. Be sure to check out the repo to follow along!
Before We Begin
This week, we are going to be diving into developing with React and Redux. Before we get started, I want to explain the reason behind some of the choices in how we are going to build our application. The React component of this tutorial serves as a bridge for those coming from a 'vanilla' HTML/CSS/JS deployment of Flask. The learning curve behind some aspects of React can be daunting to newer users, especially in the context of integrating with a Flask backend. Therefore, I try to keep as many familiar parts as possible to flatten the learning curve and get you up and running with full-stack development with Stream as quickly as possible. As a warning, some of the concepts we use are out of convenience, so be sure to air on the side of caution when deploying a production application.
First, we navigate to our React application. Our React app is based in our static directory, which we can jump to in the command line using 'cd app/static'.
Since this project is complex and involves multiple pages (or collections of components based on routes), we need to install React-Router, which handles rendering components based on a URL path. We can do this step with ‘npm install react-router-dom` in the CLI. Next, we will create a JSX file to specifically handle the routing in our application, named AppRouter (in 'app/static/js/AppRouter.jsx')
Managing state throughout multiple components in our application is a central challenge in creating our app. Rather than trying to pass that state through multiple components and attempting to keep track of those changes, it would be easier and cleaner to use a state management library such as Redux. If you are new to React, Redux can seem like a complicated concept, but the idea is quite simple. Rather than trying to manage state and its changes by passing it through multiple components individually, Redux allows you to have a centralized store to save and access state, updating multiple components simultaneously.
Using Redux involves a fair bit of programming overhead. We'll start by installing the modules with ‘npm install react-redux redux'.
We want to give our application a name that users see when they first access the site. This aspect creates a convenient entry point for setting up Redux. Redux modifies state with functions known as reducers. Reducers take an action (dispatched by a component) and update the stored state. Don't stress too much if these concepts don't make sense immediately- it all becomes more clear as the tutorial goes on.
We create our first reducer in a reducers directory (in `app/static/js/reducers/common.jsx)
This file creates a default state object, appName, and passes it to the reducer to create the initial state.
Next, we create a store file that combines any reducers that we create, which are then exported to use in a provider (in 'app/static/js/store.jsx')
A provider gives access to the state that we have established as a context throughout the components of our application. We can wrap our AppRouter tag in a newly created JSX file named App to allow our application to access the store. (in ‘app/static/js/App.jsx’)
The provider supplies the store that we just created to the rest of the application, allowing components to dispatch events, as well as retrieve state from the store. After, we import the App in our index file to render our application to the page (in 'app/static/js/index.jsx).
Now that we have created the store and provided it to the app, we can start working to access those elements. React retrieves elements from the store by using MapStateToProps, and connecting that function with the component. We implement this in our AppRouter component so you can get a feel for how it works (in 'app/static/js/AppRouter.jsx`).
In the MapStateToProps function, you can see that we associate 8appName with the value that we initialized in our common reducer. After connecting the mapping with the AppRouter* component, we can now access that state as a property.
Now that we have these basics covered, we can start moving into our first "true" component of the application: the header. The header displays our app title, as well as a conditional rendering for login, registration, settings, and the user profile dependent on whether or not a user is authenticated. To keep ourselves organized, we create a new 'components folder' with a header component contained within it. As we don’t currently have any methods for authentication, we need to create our LoggedOutView variable to render (in ‘app/js/static/js/components/Header.jsx’).
Once that step is done, we can import this component to AppRouter to be rendered. Since we call on the currentUser prop within the LoggedOutView, we will add it in MapStateToProps(in
Now that we have currentUser, we need a way to 'provide' it. First, we create a way to request information from our backend. We use the Superagent library to handle our requests, which allows us to avoid any difficulties from the deprecation of the request. To install it, we use ‘npm install superagent superagent-promise' in our CLI.
Next, we create a file to hold the functions for making requests (in
This file contains the root URL for our backend, constructors for our requests, and a handler for the response. I have jumped ahead on a few of our endpoints; however, when simultaneously developing both frontend and backend, you often find yourself in chicken-and-egg scenarios like this one. I personally prefer to create the frontend components first and then circle back to create the endpoints to support it. Additionally, for our 'POST' method, I have added our information as a form instead of JSON. This information will make a lot more sense once we head back to our backend.
A Redux Redux
Since we are handling state changes from our authentication requests, we create an auth reducer for them (in
This step handles actions from updating our form, as well as submitting the form to the server, and any errors that are returned by it.
We have to make a few changes to our common reducer as well for these actions (in
Getting In The Middle
Once a user is registered, or later logged in/out, we need a way to connect the requests with our store and its corresponding actions. We also need to be able to store the user object that we receive from the backend in the local storage of the browser to help persist the session and avoid repeated requests (in
Now that we have created our middleware and auth reducer, we need to apply it to our store. We also have a bit of housework to take care of in accommodating React-Router with Redux, as well as creating a hash history to help us navigate redirects following a successful registration. First, we install the packages in the CLI with *‘npm install react-router-redux history
*. Next, we create the file to use these packages (in app/static/js/store.jsx`).
Our registration component will encompass many of the topics we’ve covered already, like connect and mapStateToProps, as well as introducing some new ones, like mapDispatchToProps and form (in
mapDispatchToProps is where we create functions that 'dispatch' events to our store, updating the state. Each event that is dispatched has an associated 'type'', and typically values that are associated with it have to be updated. The event is sent to the reducer, which, using the switch statement, performs the relevant action. UPDATE_FIELD_AUTH adjusts the values stored for each individual input, while REGISTER takes the form and sends it to our backend using our agents. Finally, mapStateToProps keeps the props in the component synced to those changes in the state.
Forms are obviously quite a bit more involved in React if you are coming from HTML, and the learning curve for them can be obnoxiously steep for what seems like a simple element. But since nearly every site you make includes at least one, you get used to the logic rather quickly.
While our form has some simple client-side validation for input values, it's always best practice to have some kind of server-based validation as well, as a malicious actor could simply send a form request to the backend directly. We need to set up server-side validation logic soon, but before we do, we can prepare ourselves on the frontend to handle them with a new component (in
Errors always display through a component (the one that is making the request) so we can pass the state directly through props. After this step, the logic is simple: if there are errors, map them and render the key with the error. Next we must integrate it with our Register component (in
Finally, we add our registration component to AppRouter. We also need to add a mapDispatchToProps function and connect it to the component to handle redirect actions coming from registration to avoid an infinite loop of redirection (in
The componentDidMount lifecycle method checks to see if a user session is already active and load that user into state when the site is rendered.
Logged In View
Since a user can now register, theoretically, they are logged in once the registration process is over. Before we head to the backend to create our server-side endpoints, we can quickly create these different views to help let us know that the application is loading and handling the currentUser property properly. We start with the Header (in 'app/static/js/components/Header.jsx')
After, we create a home component to greet our users, with their username conditionally rendered once authenticated (in 'app/static/js/components/Home.jsx').
Finally, we will import this module to AppRouter and insert it into our render (in 'app/static/js/AppRouter.jsx').
Now that we have created this section of the frontend for the project, we can start to put together the endpoints on the backend to service the requests from the front. First, create an auth directory and initialize it (in 'app/auth/init.py').
Server Side Validation!
Next, we need to handle our registration form using Flask-WTF. Setting our inputs as a form in our requests allows us to use Flask-WTF to validate these requests on the server-side, saving us tons of time creating our own logic for it.
We are trying to keep the learning curve as low as possible; therefore, I have manually set the CSRF token to false. However, if you were looking to deploy into production, simply pass the CSRF token as a jinja2 variable in the index, retrieving and setting it as a X-CSRFToken header for any 'POST' requests. Other than that, our form will be remarkably similar to before (in 'app/auth/forms.py').
Views and to_json()
As we are no longer returning templates, the responses will have to be given as JSON instead. We can provide a User class method to convert our entry to JSON with all the relevant information needed. I have also included the Stream User Token, which is removed in the endpoint if the user requested isn't the current user to prevent abuse. As a nice feature in newer versions of Flask (1.1.0 or higher), dictionaries are automatically JSON-ified when returned.
After, we can create our registration and confirmation endpoints (in 'app/auth/views.py')
While we are returning errors from server-side form validation, we should also set up more generalized errors for incomplete/malformed requests (in 'app/auth/errors').
Flash Not Found
Using React instead of returning individual templates for each view creates new kinds of issues in trying to display flash messages. Luckily, there is a convenient Python library to handle this exact problem, Flask-Toastr. Instead of displaying as a flashed message, it displays as a neatly-styled toast, which is essentially an ephemeral notification. We can install this library in our highest project-level directory with 'pip install flask-toastr'. After, we import it as an extension (in 'app/extensions.py').
The next step is to initialize it in the main app folder, integrating our newly created auth blueprint (in 'app/init.py').
Finally, we include it in our index.html file (in 'app/static/index.html).
Pulled Up By Your Bootstrap
In the previous entry, I have also imported Bootstrap 4 to style our components. There are some conflicts in how Bootstrap interacts with React, but in our case, we take a bit of a shortcut. Using a much more complete library like Reactstrap would be outside the scope of this series. However, I definitely recommend checking it out when you get the chance!
As you likely noticed, we created a confirmation functionality similar to what we used in the previous version of Offbrand. That means that we have to send a confirmation email to the user to verify their email address. As with last time, we need to create an HTML template (in 'app/static/templates/email/change_email.html').
For those users whose email won't accept HTML, we give a text-based variant as well (in 'app/static/templates/email/change_email.txt').
Persisting the session on the front end, we also need to provide an endpoint for a currently authenticated user who has been issued a cookie to retrieve their information (in 'app/main/views.py').
We are finished creating the registration flow for both front and backends of our application. We have also created a thorough primer on React-Redux and handling requests. We can test the app is working by navigating to the static directory with the command line and running 'npm run watch', before opening another CLI to the main Flask app and using 'flask run'.
We have covered a lot of the overhead that comes along with making React/Redux applications, as well as started to create the authentication flow with a styling system. In the next few weeks, we are going to dive a lot deeper into creating the components of the site. Next week, we will cover the rest of authentication and user settings, as well as user profiles.
As always, thanks for reading, and happy hacking!
Note: The next post in this series can be found here