DEV Community 👩‍💻👨‍💻

Cover image for Introducing The FAR3 Stack: A Versatile Toolkit For Web Development.
Mahmoud Harmouch
Mahmoud Harmouch

Posted on • Updated on

Introducing The FAR3 Stack: A Versatile Toolkit For Web Development.

So, you know the gist of the FARR stack, right? A couple of days ago, I introduced the FARR stack [0]. However, I think that the name looks redundant because of the two Rs at the front. Therefore, we can add an additional R to that acronym to add Redux into the stack, which is a convenient tool to manage the global state of any web application. Hence the abbreviation becomes FARRR, or a more compact version, FAR3; the number 3 at the front represents the three Rs (React, Redux, Redis.), which means it is less MEAN and FAR more reaching!

Having sorted that out, you start to wonder: "what in the world is the FAR3 Stack?" And that's actually what we are trying to figure out through this article.

Note: This article assumes that you are already familiar with React and its ecosystem.

👉 Table Of Contents (TOC).

The FAR3 Stack.

🔝 Go To TOC

When it comes to web development, few can rival the FAR3 stack. To begin with, the FAR3 stack is a set of versatile tools that provide a comprehensive solution for web development and allows developers to create high-quality, responsive websites that are, most importantly, blazingly fast. Furthermore, it provides a great way to start with web development, as it is relatively easy to learn and use. Additionally, the FAR3 stack is flexible and can be used to create websites of any size or complexity; take a recent project of mine as a good example.

Now, in case you missed that holy manifesto, The FAR3 Stack is a new web development stack that includes four main components:

  • Frontend: React, Redux.
  • Backend: FastAPI.
  • Database: Redis.

These components work well together to provide robust and blazingly fast web applications. The FAR3 Stack is an excellent choice for web development because all stack parts are open source and free to use. Additionally, it offers excellent performance and scalability.

While each technology has its own benefits, the stack as a whole provides several advantages for web developers.

Benefits.

🔝 Go To TOC


Stackoverflow, react tag [1].

There are many benefits to using the FAR3 Stack. Perhaps the most obvious one is that React and Redux are well-known JavaScript libraries frequently used in web development. This means that a large community of developers already familiar with React and Redux can provide support and assistance when needed. Moreover, React is a declarative, efficient, and flexible JavaScript library for building user interfaces.


FastAPI, parallel burgers [2].

Additionally, FastAPI is a modern, lightweight, and efficient web framework that makes it easy to build server-side applications. FastAPI also supports async, making it easy to take advantage of the benefits of asynchronous programming. This makes it ideal for powering server-side applications.


Scaling shards and nodes [3].

Another advantage of using the FAR3 Stack is that it is relatively easy to scale. Redis, in particular, is designed for horizontal scaling, which means it can easily handle increased traffic without significant performance degradation. This makes the FAR3 Stack a good choice for developing large-scale web applications. This can also help you keep costs down and ensure that your application can handle future growth. Additionally, Redis offers a rich set of features that make it easy to work with data. With Redis, you can easily store and query data, as well as index and aggregate data [4]. As a result, Redis is an ideal database for powering server-side applications built with the FAR3 stack.

And last but not least, the FAR3 Stack is an excellent choice for those who want complete control over their application. With the FAR3 Stack, you can customize every aspect of your application to meet your specific business needs. This gives you full control over your development environment.

In essence, the FAR3 stack is a great choice for web development because it offers a wide range of benefits. It's relatively easy to use, fast, and scalable and has a large community of users and developers. So if you're looking for a technology stack that can help you build great web applications, the FAR3 stack is an excellent option to consider.

Downsides.

🔝 Go To TOC

There are definitely some Downsides to using the FAR3 stack. One of the biggest is that it requires two programming languages, JavaScript for front-end and python for backend development. It can be a bit challenging to master both languages simultaneously. For example, developers do not need to learn multiple languages or frameworks to build a complete web application. In addition, it can be difficult to debug your application - especially if you're not well-versed in both languages.


A steep learning curve.

Another one, the learning curve can be really steep – you'll need to be proficient in React, Fastapi, and Redis before you can even get started. But if you're just starting out, be aware that there may be a bit of a learning curve.

Yet, another downside is that this stack relies on React, which can be slow to load and render pages compared to other web development frameworks [5]. However, over recent years, React has become mature enough and blazingly fast by adapting new principles like lazy loading, automatic batching, and stuff of that nature [6].

Finally, the FAR3 stack can be overkill for simple projects that don't require the whole gamut of features provided by React, FastAPI, and Redis. Other stacks might be a better fit if you're looking for a more lightweight solution.

That noted, whether you want to create a simple website or a complex enterprise application, the FAR3 Stack has the tools you need to get the job done. So now, let's see how we can use this stack in action.

In the following sections, and subsequent articles, we'll start by setting up our project using the create-react-app package and installing all the dependencies needed. Once that's done, we'll set up our front-end, back-end, and database. Finally, we'll put everything together and create our full-stack login/register app. With that noted, let's dive right in!

Getting Started.

🔝 Go To TOC

Before starting, you must have Node.js installed on your machine. If you need help, check out the official docs to install Node.js and npm. Once these prerequisites are installed, you can create a new project by initializing it using create-react-app.

If you've never used create-react-app before, it is a tool that will help us quickly set up a React project. To use it, simply type the following command into your terminal:

npx create-react-app login-register-app
Enter fullscreen mode Exit fullscreen mode

This will create a new directory called login-register-app, which contains all of the files necessary for a React project. Once it's finished creating the project, change into the new directory and start the development server with the following commands:

cd login-register-app
npm start
Enter fullscreen mode Exit fullscreen mode

Open a new tab in your browser, and you should see the default create-react-app page.

Project Requirements.

🔝 Go To TOC

When setting up a new project, specific requirements must be met for the project to run smoothly. For example, our login/register system has the following workflow:

  1. On the register page, the user must fill in personal information, such as first name, last name, email address, and password. Validation will be required for all fields. The form should also have a submit button.

  2. When the form is submitted, a POST request should be made to the following endpoint: http://localhost:8000/api/v1/auth/register

  3. If the request is successful, the user should be redirected to the login page. Otherwise, an error message should be displayed on the form.

  4. On the login page, a user needs to provide an email and password for their newly created account to log in.

  5. When the form is submitted, a Get request should be made to the following endpoint: http://localhost:8000/api/v1/auth/login

  6. If the request is successful, the user should be redirected to the home page. The user can see all the website's features on the home page.

  7. That's it! You now have a basic login/register form built with React!

Once you have understood these requirements, you can begin setting up your project.

Implementation.

🔝 Go To TOC

When it comes to start coding a React project, it's important to have a well-organized folder structure. This will make it easier to keep track of your files and code. To do so, create a new folder called "components" inside your "src" folder. This is where all of your React components will go.

Next, create another folder called "SignUp" in your src folder that represents our SignUp component. Within that folder, create a new file called index.js. This will be the main React component for our register form.

Having done that, remove all the unecessairy files and our project structure will look like the following:

.
├── package.json
├── package-lock.json
├── public
│    ├── favicon.ico
│    ├── index.html
│    ├── logo192.png
│    ├── logo512.png
│    ├── manifest.json
│    └── robots.txt
└── src
     ├── components
     │    └── SignUp
     │         └── index.js
     ├── App.js
     └── index.js
Enter fullscreen mode Exit fullscreen mode

SignUp Component.

🔝 Go To TOC
SignUp page.

After the project folder has been set up, the next step is to create a React component that will be used as the SignUp form. The SignUp form component should have four state objects: firstName, lastName, email, and password. We also need four additional state objects to handle error messages and render them on the UI: firstNameError, lastNameError, emailError, passwordError. These fields can be modeled by MUI components, meaning their values should be set by the value attribute of the element or using a pure html tag. But we will use MUI because we want something that looks elegant!

Now, we need to install MUI. To do so, head to the official MUI docs and follow the instructions. Essentially, all we need to do is to run the following command:

npm i @mui/material @mui/styled-engine-sc styled-components @mui/icons-material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Having installed the MUI library on your machine, you can now create the following TextField component:

Show Code
import React, { useState } from "react";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import MailIcon from "@mui/icons-material/Mail";
import IconButton from "@mui/material/IconButton";

const SignUp = () => {

  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState("");

  return (
    <div>
      <TextField
        fullWidth
        className="text-field-root"
        variant="outlined"
        label="Email Address"
        value={email}
        onChange={(e) => {
          setEmail(e.target.value);
          setEmailError("");
        }}
        helperText={emailError}
        InputProps={{
          startAdornment: (
            <InputAdornment position="start" variant="standard">
              <IconButton aria-label="Email" edge="end" disabled>
                <MailIcon />
              </IconButton>
            </InputAdornment>
          ),
        }}
      />
    </div>
  );
};

export default SignUp;
Enter fullscreen mode Exit fullscreen mode

Now we need to import this component into our App.js file:

Show Code
import SignUp from "./components/SignUp"

const App = () => {
  return (
    <div className="App">
      <SignUp />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Running npm start will result in the following component being rendered on a web browser tab:

TextField Component.

For more info about different props of the TextField component, you can refer to the official documentation.

Having that working nice and dandy, let's add the other three state variables described previously. Doing so will result in the following:

Show Code
import { useState } from "react";
import { Box } from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import PermIdentityIcon from "@mui/icons-material/PermIdentity";
import PersonIcon from "@mui/icons-material/Person";
import MailIcon from "@mui/icons-material/Mail";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from '@mui/icons-material/VisibilityOff';

const SignUp = () => {
  const [values, setValues] = useState({
    firstName: "",
    lastName: "",
    email: "",
    password: "",
    showPassword: false,
  });

  const [errorValues, setErrorValues] = useState({
    firstNameError: "",
    lastNameError: "",
    emailError: "",
    passwordError: "",
  });


  const handleChange = (prop) => (event) => { 
    setValues({ ...values, [prop]: event.target.value });
  };

  const handleErrorChange = (prop) => (event) => {
    if (typeof event == "string") setErrorValues({ ...errorValues, [prop]: event });
    else setErrorValues({ ...errorValues, [prop]: event.target.value });
  };

  const handleClickShowPassword = () => {
    setValues({
      ...values,
      showPassword: !values.showPassword,
    });
  };

  const handleMouseDownPassword = (event) => {
    event.preventDefault();
  };

  return (
    <Box
      component="form"
      sx={{ "& .MuiTextField-root": { m: 1, width: "25ch" } }}
      display="flex"
      flexDirection="column"
      alignItems="center"
      noValidate
      autoComplete="off"
      onClick={() => {}}
    >
      <Typography component="div" variant="h4" mb={3}>
        Create an account
      </Typography>
      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        width="40ch"
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="First Name"
          value={values.firstName}
          onChange={(e) => {
            handleChange("firstName")(e);
            handleErrorChange("firstNameError")("");
          }}
          helperText={errorValues.firstNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="First Name" edge="end" disabled>
                  <PermIdentityIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
        <TextField
          fullWidth
          variant="outlined"
          label="Last Name"
          value={values.lastName}
          onChange={(e) => {
            handleChange("lastName")(e);
            handleErrorChange("lastNameError")("");
          }}
          helperText={errorValues.lastNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Last Name" edge="end" disabled>
                  <PersonIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="Email Address"
          value={values.email}
          onChange={(e) => {
            handleChange("email")(e);
            handleErrorChange("emailError")("");
          }}
          helperText={errorValues.emailError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Email" edge="end" disabled>
                  <MailIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="Password"
          value={values.password}
          onChange={(e) => {
            handleChange("password")(e);
            handleErrorChange("passwordError")("");
          }}
          helperText={errorValues.passwordError}
          InputProps={{
            endAdornment:(
              <InputAdornment position="end">
                <IconButton
                  aria-label="toggle password visibility"
                  onClick={handleClickShowPassword}
                  onMouseDown={handleMouseDownPassword}
                  edge="end"
                >
                  {values.showPassword ? <VisibilityOff /> : <Visibility />}
                </IconButton>
              </InputAdornment>
          )
          }}
        />
      </Box>

      <Box
        display="flex"
        alignItems="center"
        justifyContent="space-between"
        mt={3}
        mb={3}
      >
        <Button variant="contained" color="primary">
          {"Sign Up"}
        </Button>
      </Box>
      <Box display="flex" alignItems="center" justifyContent="space-between">
        <Typography>Have an account? Sign In</Typography>
      </Box>
    </Box>
  );
};

export default SignUp;
Enter fullscreen mode Exit fullscreen mode

The last step is to wire up the register form component to submit the first_name, last_name, email, and password to the server when the form is submitted. This can be done by adding an onClick handler to the outermost <Box> component. In the onClick handler, you need to call a function that sends an HTTP request to the server with the form's data. For example, by adding the onClick handler, our code will result in the following listing:

Show Code
import { useState } from "react";
import { Box } from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import PermIdentityIcon from "@mui/icons-material/PermIdentity";
import PersonIcon from "@mui/icons-material/Person";
import MailIcon from "@mui/icons-material/Mail";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from '@mui/icons-material/VisibilityOff';

const SignUp = () => {
  const [values, setValues] = useState({
    firstName: "",
    lastName: "",
    email: "",
    password: "",
    showPassword: false,
  });

  const [errorValues, setErrorValues] = useState({
    firstNameError: "",
    lastNameError: "",
    emailError: "",
    passwordError: "",
  });


  const handleChange = (prop) => (event) => { 
    setValues({ ...values, [prop]: event.target.value });
  };

  const handleErrorChange = (prop) => (event) => {
    if (typeof event == "string") setErrorValues({ ...errorValues, [prop]: event });
    else setErrorValues({ ...errorValues, [prop]: event.target.value });
  };

  const handleClickShowPassword = () => {
    setValues({
      ...values,
      showPassword: !values.showPassword,
    });
  };

  const handleMouseDownPassword = (event) => {
    event.preventDefault();
  };

  const onClick = () => {
    if (!values.firstName) {
      handleErrorChange("firstNameError")("First Name is required!");
    } else if (!values.lastName) {
      handleErrorChange("lastNameError")("Last Name is required!");
    } else if (!values.email) {
      handleErrorChange("emailError")("Email is required!");
    } else if (!values.password) {
      handleErrorChange("passwordError")("Password is required!");
    } else {
      // await fetch('login-url')
      console.log("submit")
    }
  };

  return (
    <Box
      component="form"
      sx={{ "& .MuiTextField-root": { m: 1, width: "25ch" } }}
      display="flex"
      flexDirection="column"
      alignItems="center"
      noValidate
      autoComplete="off"
      onClick={onClick}
    >
      <Typography component="div" variant="h4" mb={3}>
        Create an account
      </Typography>
      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        width="40ch"
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="First Name"
          value={values.firstName}
          onChange={(e) => {
            handleChange("firstName")(e);
            handleErrorChange("firstNameError")("");
          }}
          helperText={errorValues.firstNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="First Name" edge="end" disabled>
                  <PermIdentityIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
        <TextField
          fullWidth
          variant="outlined"
          label="Last Name"
          value={values.lastName}
          onChange={(e) => {
            handleChange("lastName")(e);
            handleErrorChange("lastNameError")("");
          }}
          helperText={errorValues.lastNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Last Name" edge="end" disabled>
                  <PersonIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="Email Address"
          value={values.email}
          onChange={(e) => {
            handleChange("email")(e);
            handleErrorChange("emailError")("");
          }}
          helperText={errorValues.emailError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Email" edge="end" disabled>
                  <MailIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="Password"
          value={values.password}
          onChange={(e) => {
            handleChange("password")(e);
            handleErrorChange("passwordError")("");
          }}
          helperText={errorValues.passwordError}
          InputProps={{
            endAdornment:(
              <InputAdornment position="end">
                <IconButton
                  aria-label="toggle password visibility"
                  onClick={handleClickShowPassword}
                  onMouseDown={handleMouseDownPassword}
                  edge="end"
                >
                  {values.showPassword ? <VisibilityOff /> : <Visibility />}
                </IconButton>
              </InputAdornment>
          )
          }}
        />
      </Box>

      <Box
        display="flex"
        alignItems="center"
        justifyContent="space-between"
        mt={3}
        mb={3}
      >
        <Button variant="contained" color="primary">
          {"Sign Up"}
        </Button>
      </Box>
      <Box display="flex" alignItems="center" justifyContent="space-between">
        <Typography>Have an account? Sign In</Typography>
      </Box>
    </Box>
  );
};

export default SignUp;
Enter fullscreen mode Exit fullscreen mode

However, in order to adhere to the separation of concern principle, we should move API calls into a different file. Doing so will make our code more organized and easier to maintain in the long run.

Now, we will create a new file called api.js at the top-level directory along with our App component, and move the fetch/axios call into that file.

Show Code
import {
  fetchError,
  fetchStart,
  fetchSuccess,
} from "../redux/commonReducer/actions";
import axios from "axios";

const axiosJson = () => {
  return axios.create({
    baseURL: `http://localhost:8000/api/v1/`,
    headers: { "Content-Type": "application/json" },
  });
};

const axiosJsonObject = axiosJson()

export const onRegister = (formData) => {
    return (dispatch) => {
      dispatch(fetchStart());
      axiosJsonObject
        .post(
          "auth/register",
          JSON.stringify({
            first_name: formData.firstName,
            last_name: formData.lastName,
            email: formData.email,
            password: formData.password,
          })
        )
        .then(({ data }) => {
          if (data.status_code === 201) {
            localStorage.setItem("token", data.token.value);
            axiosJsonObject.defaults.headers.common["Authorization"] =
              "Bearer " + data.token.value;
            dispatch(fetchSuccess(data.message));
          } else {
            dispatch(fetchError(data.message));
          }
        })
        .catch(function (error) {
          dispatch(fetchError(""));
        });
    };
}
Enter fullscreen mode Exit fullscreen mode

Now that we have created and configured our axios object and methods in the Axios.js file, we can import it into our SignUp form component and use it from the onClick handler. The SignUp component should now look like this:

Show Code
import { useState } from "react";
import { Box } from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import PermIdentityIcon from "@mui/icons-material/PermIdentity";
import PersonIcon from "@mui/icons-material/Person";
import MailIcon from "@mui/icons-material/Mail";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { useDispatch } from "react-redux";
import { onRegister } from "../../api/Axios";

const SignUp = () => {
  const [values, setValues] = useState({
    firstName: "",
    lastName: "",
    email: "",
    password: "",
    showPassword: false,
  });

  const [errorValues, setErrorValues] = useState({
    firstNameError: "",
    lastNameError: "",
    emailError: "",
    passwordError: "",
  });

  const dispatch = useDispatch();

  const handleChange = (prop) => (event) => { 
    setValues({ ...values, [prop]: event.target.value });
  };

  const handleErrorChange = (prop) => (event) => {
    if (typeof event == "string") setErrorValues({ ...errorValues, [prop]: event });
    else setErrorValues({ ...errorValues, [prop]: event.target.value });
  };

  const handleClickShowPassword = () => {
    setValues({
      ...values,
      showPassword: !values.showPassword,
    });
  };

  const handleMouseDownPassword = (event) => {
    event.preventDefault();
  };

  const onClick = () => {
    if (!values.firstName) {
      handleErrorChange("firstNameError")("First Name is required!");
    } else if (!values.lastName) {
      handleErrorChange("lastNameError")("Last Name is required!");
    } else if (!values.email) {
      handleErrorChange("emailError")("Email is required!");
    } else if (!values.password) {
      handleErrorChange("passwordError")("Password is required!");
    } else {
      dispatch(onRegister(values))
    }
  };
  return (
    <Box
      component="form"
      sx={{ "& .MuiTextField-root": { m: 1, width: "25ch" } }}
      display="flex"
      flexDirection="column"
      alignItems="center"
      noValidate
      autoComplete="off"
      onClick={onClick}
    >
      <Typography component="div" variant="h4" mb={3}>
        Create an account
      </Typography>
      <Box
        display="flex"
        flexDirection={{ xs: "column", md: "row" }}
        width="40ch"
        alignItems="center"
      >
        <TextField
          fullWidth
          variant="outlined"
          label="First Name"
          value={values.firstName}
          onChange={(e) => {
            handleChange("firstName")(e);
            handleErrorChange("firstNameError")("");
          }}
          helperText={errorValues.firstNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="First Name" edge="end" disabled>
                  <PermIdentityIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
        <TextField
          fullWidth
          variant="outlined"
          label="Last Name"
          value={values.lastName}
          onChange={(e) => {
            handleChange("lastName")(e);
            handleErrorChange("lastNameError")("");
          }}
          helperText={errorValues.lastNameError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Last Name" edge="end" disabled>
                  <PersonIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box>
        <TextField
          fullWidth
          variant="outlined"
          label="Email Address"
          value={values.email}
          onChange={(e) => {
            handleChange("email")(e);
            handleErrorChange("emailError")("");
          }}
          helperText={errorValues.emailError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Email" edge="end" disabled>
                  <MailIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box>
        <TextField
          fullWidth
          variant="outlined"
          label="Password"
          value={values.password}
          onChange={(e) => {
            handleChange("password")(e);
            handleErrorChange("passwordError")("");
          }}
          helperText={errorValues.passwordError}
          InputProps={{
            endAdornment:(
              <InputAdornment position="end">
                <IconButton
                  aria-label="toggle password visibility"
                  onClick={handleClickShowPassword}
                  onMouseDown={handleMouseDownPassword}
                  edge="end"
                >
                  {values.showPassword ? <VisibilityOff /> : <Visibility />}
                </IconButton>
              </InputAdornment>
          )
          }}
        />
      </Box>

      <Box
        display="flex"
        alignItems="left"
        justifyContent="space-between"
        mt={3}
        mb={3}
      >
        <Button variant="contained" color="primary">
          {"Sign Up"}
        </Button>
      </Box>
      <Box display="flex" alignItems="left" justifyContent="space-between">
        <Typography>Have an account? Sign In</Typography>
      </Box>
    </Box>
  );
};

export default SignUp;
Enter fullscreen mode Exit fullscreen mode

Redux Application Data Flow [7].

Notice the use of a dispatch function and a reducer in the follow-up section, which are objects used by redux to manage various states of our app, which can be helpful for debugging purposes. Essentially, a dispatcher is a mechanism by which actions can communicate with the store. When an action is dispatched, like fetchStart, the dispatcher will call upon the reducer function to update the application's state accordingly. Now, it is time to create the reducer mentioned above, in this case, commonReducer. To do so, let's create a redux folder structured in the following manner:

└── redux
    ├── commonReducer
    │    ├── actions
    │    │    └── index.js
    │    ├── index.js
    │    └── selectors
    │         └── index.js
    ├── rootReducer.js
    └── store.js
Enter fullscreen mode Exit fullscreen mode

In the redux/commonReducer/actions/index.js file, we will define all of the actions that we will be able to dispatch. For our example, we have used three actions:

Show Code
import {
  FETCH_ERROR,
  FETCH_START,
  FETCH_SUCCESS,
} from "../../../constants/ActionTypes";

export const fetchSuccess = (message) => {
  return (dispatch) => {
    dispatch({
      type: FETCH_SUCCESS,
      payload: message || "",
    });
  };
};
export const fetchError = (error) => {
  return (dispatch) => {
    dispatch({
      type: FETCH_ERROR,
      payload: error,
    });
  };
};

export const fetchStart = () => {
  return (dispatch) => {
    dispatch({
      type: FETCH_START,
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

Now, we need to create our reducer redux/commonReducer/index.js. A reducer is a mapping of how different actions should affect the application's state. In this case, we have two states, message, and errorMessage. This will result in the following reducer:

Show Code
import {
  FETCH_ERROR,
  FETCH_START,
  FETCH_SUCCESS,
} from "../../constants/ActionTypes";

const initialState = {
  error: "",
  message: "",
  loading: false,
};

const commonReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_START: { 
      // we use the spread operator `...` to make sure we don't mutate the state directly. We want to return a new state object every time an action is dispatched. 
      return { ...state, error: "", message: "", loading: true };
    }
    case FETCH_SUCCESS: {
      return { ...state, error: "", loading: false, message: action.payload };
    }
    case FETCH_ERROR: {
      return { ...state, loading: false, message: "", error: action.payload };
    }
    default:
      // don't forget to add the default case which will just return the state if no actions were matched
      return state;
  }
};

export default commonReducer;
Enter fullscreen mode Exit fullscreen mode

By utilizing both a dispatcher and a reducer, we can have much more granular control over our application state management. Again, as mentioned previously, this can be extremely helpful for debugging purposes and maintaining a consistent state across different parts of the application.

The commonReducer/selectors/index.js file is a selector used when we need to select/access the value of a state variable within a component of our app, hence the name selector. As you can tell, we have three selectors:

export const message = (state) => state.common.message;
export const loading = (state) => state.common.loading;
export const error = (state) => state.common.error;
Enter fullscreen mode Exit fullscreen mode

Each time we want to access the value of a given app state, say message, we can use useSelector from the redux module, like the following:

Show Code
import { useSelector } from 'react-redux';
import {message} from "../../redux/commonReducer/selectors"

const Message = () => {
  const message = useSelector(message);

  return (
    <div>{message}</div>
  );
};

export default Message;
Enter fullscreen mode Exit fullscreen mode

Note that we have created a reducer that will help us to know whether or not an API call was successful. For instance, let's go back to our onRegister API method:

Show Code

export const onRegister = (formData) => {
    return (dispatch) => {
      dispatch(fetchStart());
      axiosJsonObject
        .post(
          "auth/register",
          JSON.stringify({
            first_name: formData.firstName,
            last_name: formData.lastName,
            email: formData.email,
            password: formData.password,
          })
        )
        .then(({ data }) => {
          if (data.status_code === 201) {
            localStorage.setItem("token", data.token.value);
            axiosJsonObject.defaults.headers.common["Authorization"] =
              "Bearer " + data.token.value;
            dispatch(fetchSuccess(data.message));
          } else {
            dispatch(fetchError(data.message));
          }
        })
        .catch(function (error) {
          dispatch(fetchError(""));
        });
    };
}
Enter fullscreen mode Exit fullscreen mode

  • fetchStart will tell us that this API method has been called.
  • fetchSuccess will set the value of the message on a successful API call.
  • fetchError will set the value of the message if an exception is being thrown during the API call or on a failed attempt.

This way, when debugging the app, we can tell what went wrong during the API call.

With that noted, it is time to configure our redux store. Now, create a file named rootReducer that is configured in the following manner:

Show Code
import { combineReducers } from "redux";
import commonReducer from "./commonReducer";

const rootReducer = combineReducers({
  common: commonReducer,
});

export default rootReducer;
Enter fullscreen mode Exit fullscreen mode

Note the key common that is used to access a state in our selector:

export const message = (state) => state.common.message;
Enter fullscreen mode Exit fullscreen mode

And finally, the redux/store.js is configured in the following way:

Show Code
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./rootReducer";

const bindMiddleware = (middleware) => {
  if (process.env.NODE_ENV !== "production") {
    const { composeWithDevTools } = require("redux-devtools-extension");
    return composeWithDevTools(applyMiddleware(...middleware));
  }
  return applyMiddleware(...middleware);
};
const store = createStore(rootReducer, bindMiddleware([thunk]));

export default store;
Enter fullscreen mode Exit fullscreen mode

Now, we need to wrap our app with a redux provider to make redux work in our app:

Show Code
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

We then need to install the missing dependencies:

npm i axios redux-devtools-extension react-router-dom redux-thunk react-redux
Enter fullscreen mode Exit fullscreen mode

After installing these libraries, we can run our app:

npm start
Enter fullscreen mode Exit fullscreen mode

If you've been paying attention, you may have noticed that we have installed the redux-devtools-extension package. This powerful tool allows us to debug our React app more efficiently.

If you're not familiar with the extension, I highly recommend taking some time to check it out. It's a great way to understand better how Redux works and can save you a lot of time when debugging your apps.

With the redux-devtools-extension, we can now easily see all the actions that have been dispatched, as well as the current state of our store. This extension also allows us to time travel, meaning we can go back and forth through different states to see how our application got to where it is, as shown in the image below.

Redux devtools extension.

Now that we have created the signup component, it is time to create the signin component. This will allow us to complete the authentication process for our users. The signin component will take care of verifying the user's credentials and, if they are correct, authenticating the user. Once the user is authenticated, they can access the authorized areas of our website.

As before, we'll use react and redux to implement this functionality. Once again, the goal is to create a simple, elegant component that allows users to sign in quickly and easily. With that in mind, let's get started!

SignIn Component.

🔝 Go To TOC

The first thing we need to do is create a react component for our sign-in component. As with the signup component, we'll need to store the email and password as state objects. In addition, we'll need to create a signin action in our redux store that dispatches the API calls to submit user credentials to the server. Finally, you'll need to create an Auth reducer that handles the response from the server and sets auth states accordingly.

Let's start by creating a SignIn component under the components directory. To do so, create a new directory called SignIn and within that directory, create a file called index.js which will contain the following logic for our SignIn component:

Show Code
import { useState } from "react";
import { Box } from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import MailIcon from "@mui/icons-material/Mail";
import LockIcon from '@mui/icons-material/Lock';
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { useDispatch } from "react-redux";
import { onSignIn } from "../../api/Axios";

const SignIn = () => {
  const [values, setValues] = useState({
    email: "",
    password: "",
    showPassword: false,
  });

  const [errorValues, setErrorValues] = useState({
    emailError: "",
    passwordError: "",
  });

  const dispatch = useDispatch();

  const handleChange = (prop) => (event) => { 
    setValues({ ...values, [prop]: event.target.value });
  };

  const handleErrorChange = (prop) => (event) => {
    if (typeof event == "string") setErrorValues({ ...errorValues, [prop]: event });
    else setErrorValues({ ...errorValues, [prop]: event.target.value });
  };

  const handleClickShowPassword = () => {
    setValues({
      ...values,
      showPassword: !values.showPassword,
    });
  };

  const handleMouseDownPassword = (event) => {
    event.preventDefault();
  };

  const onClick = () => {
    if (!values.email) {
      handleErrorChange("emailError")("Email is required!");
    } else if (!values.password) {
      handleErrorChange("passwordError")("Password is required!");
    } else {
      dispatch(onSignIn(values))
    }
  };
  return (
    <Box
      component="form"
      sx={{ "& .MuiTextField-root": { m: 1, width: "25ch" } }}
      display="flex"
      flexDirection="column"
      alignItems="center"
      noValidate
      autoComplete="off"
      onClick={onClick}
    >
      <Typography component="div" variant="h5" mb={3}>
        Login to your account
      </Typography>

      <Box>
        <TextField
          fullWidth
          variant="outlined"
          label="Email Address"
          value={values.email}
          onChange={(e) => {
            handleChange("email")(e);
            handleErrorChange("emailError")("");
          }}
          helperText={errorValues.emailError}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Email" edge="end" disabled>
                  <MailIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Box>
        <TextField
          fullWidth
          type={values.showPassword ? 'text' : 'password'}
          variant="outlined"
          label="Password"
          value={values.password}
          onChange={(e) => {
            handleChange("password")(e);
            handleErrorChange("passwordError")("");
          }}
          helperText={errorValues.passwordError}
          InputProps={{
            endAdornment:(
              <InputAdornment position="end">
                <IconButton
                  aria-label="toggle password visibility"
                  onClick={handleClickShowPassword}
                  onMouseDown={handleMouseDownPassword}
                  edge="end"
                >
                  {values.showPassword ? <VisibilityOff /> : <Visibility />}
                </IconButton>
              </InputAdornment>
          ),
            startAdornment: (
              <InputAdornment position="start" variant="standard">
                <IconButton aria-label="Email" edge="end" disabled>
                  <LockIcon />
                </IconButton>
              </InputAdornment>
            )
          }}
        />
      </Box>

      <Box
        display="flex"
        alignItems="left"
        justifyContent="space-between"
        mt={3}
        mb={3}
      >
        <Button variant="contained" color="primary">
          {"Sign Up"}
        </Button>
      </Box>
      <Box display="flex" alignItems="left" justifyContent="space-between">
        <Typography>Don't have an account? Sign Up</Typography>
      </Box>
    </Box>
  );
};

export default SignIn;
Enter fullscreen mode Exit fullscreen mode

Similarly to the signup component, we need to create an API call to submit user credentials to the server. However, we also need to create an auth reducer to dispatch actions to the store and set the authUser state. This authUser state will be an object that represents whether or not a user is logged in. If they are logged in, we'll set its value to a user object returned from the server, and if they're not, we'll set it to null.

When a user signs in to our app, they need to provide some credentials, which the API then sends off to the server to verify. Once the server has verified the user's credentials, it will send back a token that can be stored in local storage and used to authenticate future requests.

The authentication reducer will be responsible for dispatching actions to store and setting the authUser state. This state will determine whether or not a user is signed in. The authentication reducer will also be responsible for handling any errors that occur during sign-in.

Now, it is time to create the reducer described above; in this case, let's call it authReducer. To do so, let's create a redux folder structured in the following manner:

src
 └── redux
     └── authReducer
          ├── actions
          │    └── index.js
          ├── index.js
          └── selectors
               └── index.js
Enter fullscreen mode Exit fullscreen mode

In the actions/index.js, we need to have the following logic:

Show Code
import {
  UPDATE_AUTH_USER,
} from "../../../constants/ActionTypes";

export const setAuthUser = (user) => {
  return (dispatch) => {
    dispatch({
      type: UPDATE_AUTH_USER,
      payload: user,
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

The reducer should look like the following:

Show Code
import {
  UPDATE_AUTH_USER
} from "../../constants/ActionTypes";

const initialState = {
  authUser: "",
};

const authReducer = (state = initialState, action) => {
  switch (action.type) {
    case UPDATE_AUTH_USER: {
      return {
        ...state,
        authUser: action.payload,
        loadUser: true,
      };
    }
    default:
      return state;
  }
};

export default authReducer;
Enter fullscreen mode Exit fullscreen mode

And finally, we have our auth selecter:

export const authUser = (state) => state.auth.authUser;
Enter fullscreen mode Exit fullscreen mode

Now, we need to combine the auth reducer with other reducers, which is the commonReducer:

Show Code
import { combineReducers } from "redux";
import authReducer from "./authReducer";
import commonReducer from "./commonReducer";

const rootReducer = combineReducers({
  common: commonReducer,
  auth: authReducer,
});

export default rootReducer;
Enter fullscreen mode Exit fullscreen mode

And that's pretty much it. We have fully configured our new auth reducer in redux.

Next, we need to create our onSignIn API endpoint to submit user credentials to the server. Doing so, we will end up with the following logic:

Show Code
export const onSignIn = (formData) => {
    return (dispatch) => {
      dispatch(fetchStart());
      axiosJsonObject
        .post(
          "auth/register",
          JSON.stringify({
            first_name: formData.firstName,
            last_name: formData.lastName,
            email: formData.email,
            password: formData.password,
          })
        )
        .then(({ data }) => {
          if (data.status_code === 201) {
            localStorage.setItem("token", data.token.value);
            axiosJsonObject.defaults.headers.common["Authorization"] =
              "Bearer " + data.token.value;
            dispatch(fetchSuccess(data.message));
          } else {
            dispatch(fetchError(data.message));
          }
        })
        .catch(function (error) {
          dispatch(fetchError(""));
        });
    };
}
Enter fullscreen mode Exit fullscreen mode

Notice the line:

localStorage.setItem("token", data.token.value);
Enter fullscreen mode Exit fullscreen mode

This sets the token in local storage with a key "token" and the token value returned from the server.

The last step is to wire up the auth reducer and the API calls to set the authUser state for a given token. This can be done by dispatching the redux setAuthUser action.

Show Code
export const getAuthUser = (loaded = false, token, message) => {
  return (dispatch) => {
    if (!token) {
      const token = localStorage.getItem("token");
      axiosJson.defaults.headers.common["Authorization"] = "Bearer " + token;
    }
    dispatch(fetchStart());
    dispatch(updateLoadUser(loaded));
    axJson
      .get("user/profile")
      .then(({ data }) => {
        if (data.status_code === 200) {
          dispatch(fetchSuccess(message));
          dispatch(updateLoadUser(true));
          dispatch(setAuthUser(data.user));
          dispatch(setCurrentUser(data.user));
          // store the user in localStorage
          localStorage.setItem("user", JSON.stringify(data.user));
        } else {
          dispatch(updateLoadUser(true));
        }
      })
      .catch(function (error) {
        dispatch("");
      });
  };
}
Enter fullscreen mode Exit fullscreen mode

Now, we can call this method in onSignIn to automatically set a given user's value. Our onSignIn method becomes:

Show Code
export const onSignIn = (formData) => {
    return (dispatch) => {
      dispatch(fetchStart());
      axiosJsonObject
        .get(
          "auth/login",
          JSON.stringify({
            email: formData.email,
            password: formData.password,
          })
        )
        .then(({ data }) => {
          if (data.status_code === 201) {
            localStorage.setItem("token", data.token.value);
            axiosJsonObject.defaults.headers.common["Authorization"] =
              "Bearer " + data.token.value;
            dispatch(getAuthUser(data.message));            
          } else {
            dispatch(fetchError(data.message));
          }
        })
        .catch(function (error) {
          dispatch(fetchError(""));
        });
    };
}
Enter fullscreen mode Exit fullscreen mode

Now, you can navigate to localhost:3000 to see the SignIn component in action:

SignIn Component.

Configure the Routes.

🔝 Go To TOC
Photo by Matt Seymour on Unsplash

Now that we have implemented login and register components, it is time to configure the app routes. This will ensure that users can navigate to the appropriate pages when they log in or register for your app. By properly configuring the app routes, we can provide a seamless user experience for our app's users.

We can use React Router library V6 to help us with this. It makes it easy to define and manage routes in a React application.

First, we need to install React Router if it is not already installed:

npm i react-router-dom@6
Enter fullscreen mode Exit fullscreen mode

Then, we can import it into our App.js file:

import { useLocation, Routes, Route, Navigate } from "react-router-dom";
Enter fullscreen mode Exit fullscreen mode

Next, we need to define our routes. For our login and register components, we will create a /signin and /signup routes, respectively:

Show Code
import SignUp from "./components/SignUp";
import SignIn from "./components/SignIn";
import React, { useEffect, useState } from "react";
import { useLocation, Routes, Route, Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { authUser } from "./redux/authReducer/selectors";

const App = () => {
  const [currentAuthUser, setCurrentAuthUser] = useState(useSelector(authUser));
  const location = useLocation();
  const dispatch = useDispatch();

  useEffect(() => {
    if (localStorage.getItem("user")) {
      setCurrentAuthUser(JSON.parse(localStorage.getItem("user")));
    }
    // eslint-disable-next-line
  }, [dispatch, localStorage.getItem("user")]);

  if (currentAuthUser && location.pathname === "/sigin") {
    return (
      <>
        <Navigate to={"/home"} replace />
      </>
    );
  }

  return (
    <>
        <Routes>
          <Route
            exact
            path="/"
            element={<Navigate replace to="/signin" />}
          ></Route>
          <Route
            path="/home"
            element={
              <div className="home">
                {currentAuthUser ? (
                  <div> Welcome back {currentAuthUser.name}!</div>
                ) : (
                  <Navigate to={"/signin"} replace />
                )}
              </div>
            }
          ></Route>
          <Route
            path="/signup"
            element={
              <div className="signup">
                {<SignUp />}
              </div>
            }
          ></Route>
          <Route
            path="/signin"
            element={
              <div className="Signin">
                {<SignIn />}
              </div>
            }
          ></Route>
        </Routes>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Once we have configured our app routes, we can test them out to ensure they are working as intended.

Open your browser and navigate to one of the URLs:

http://localhost:3000/signup
http://localhost:3000/signin
Enter fullscreen mode Exit fullscreen mode

Wrapping Up.

🔝 Go To TOC

We've now completed the front-end side of our project. The only thing left is implementing the backend, which we will work on in the following article. This will be where we store and retrieve data from our database. We will also use the backend to process requests from our front-end. However, implementing the backend would make the article dense and hard to digest. Therefore, it is a topic for a future article.

I hope you enjoyed this part of the series and that you found it helpful in understanding how to start using the FAR3 stack. If you have any questions or comments, please feel free to reach out to me on one of my socials via my website. Thank you for reading! Stay tuned!

You can checkout my repo on Github to use/modify the code used in this article to fit your needs:

GitHub logo wiseaidev / awesome-far3

A collection of projects and resources related to the FAR3 stack.

awesome-far3

A collection of projects and resources related to the FAR3 stack.

References.

🔝 Go To TOC

[0] Mahmoud Harmouch. The FARR Stack Manifesto.. dev.to. Published 2022-08-30.

[1] StackOverFlow. React tag.. dev.to. Retrieved 2022-09-03.

[2] FastAPI. Parallel Burgers.. fastapi.tiangolo.com. Retrieved 2022-09-03.

[3] Redis. Linear Scaling with Redis Enterprise.. redis.com. Retrieved 2022-09-03.

[4] Redis. RediSearch Aggregations.. redis.io. Retrieved 2022-09-03.

[5] Henry Boisdequin. React vs Vue vs Angular vs Svelte.. dev.to. Retrieved 2022-09-03.

[6] ReactJS.org. React v18.0. reactjs.org. Retrieved 2022-09-03.

[7] redux.js.org. Redux Application Data Flow. redux.js.org. Retrieved 2022-09-03.

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await