Skip to content

Creating Front End Components

James Keary edited this page Sep 17, 2019 · 20 revisions

This document outlines the process of creating a React component and hooking it up to the backend. Our front end is comprised of React components, some of which are used many times throughout the app, for example the navigation bar up top is a component that is generally on each url, a button component will also be used several times over. Some components we use came pre made from Material-UI, making it easier for us to create the UI quickly thru leveraging other's code. Other components we need to build because they need to be a little more complex and specific to our use case; for example they maybe need to throw data around the UI or send requests back to our API. If you are familiar with React, you may recognize this concept as state in React; simply put the state is the mutable data of a component along with how that mutable data is managed. It is different from props on a component which are generally the unmutable data of the component, the things that define that component, if you will. Generally if you are creating a component that needs to pass around data, you will need to create what is known as a "stateful" component, one that has a way to manage the state. We have chosen to use React Hooks and Mobx for our state management. Each stateful component you create will need a corresponding store for storing state based information, and perhaps a corresponding api file for making api requests, and maybe their own style sheet. A stateful component, like all components can have a bunch of smaller stateless (or statefull) components within, like Material-UI buttons and table columns etc. For these instructions, I will be creating a component called the Participants Component.

Step 1 - Creating a Component

Lets say we need a component to display all the participants from the database, lets call it the participants component. The Participants component in its most basic form looks like this:

import { rootStoreContext } from "../stores/RootStore"
import Breadcrumbs from "@material-ui/core/Breadcrumbs"
import Typography from "@material-ui/core/Typography"
import Link from "@material-ui/core/Link"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import Fab from "@material-ui/core/Fab"
import AddIcon from "@material-ui/icons/Add"

const Participants = () => {
  const rootStore = useContext(rootStoreContext)
  const [participants, setParticipants] = useState([])

  useEffect(() => {
    const fetchData = async () => {
      await rootStore.ParticipantStore.getParticipants()
      setParticipants(rootStore.ParticipantStore.participants)
    }
    fetchData()
  }, [])

  return (
    <div>
      <Breadcrumbs separator="›" aria-label="breadcrumb">
        <Link color="inherit" href="/">
          Home
        </Link>
        <Typography color="textPrimary">Search Results</Typography>
      </Breadcrumbs>
      <Typography variant="h5" color="textPrimary">
        Participants
      </Typography>
      <div className="participants">
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>
                <Typography>#</Typography>
              </TableCell>
              <TableCell>
                <Typography>PPID</Typography>
              </TableCell>
              <TableCell>
                <Typography>First Name</Typography>
              </TableCell>
              <TableCell>
                <Typography>Last Name</Typography>
              </TableCell>
              <TableCell>
                <Typography>Address</Typography>
              </TableCell>
              <TableCell>
                <Typography>DOB</Typography>
              </TableCell>
              <TableCell>
                <Typography>Add</Typography>
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {participants.map((participant, index) => (
              <TableRow key={index}>
                <TableCell>
                  <Typography>Number</Typography>
                </TableCell>
                <TableCell>
                  <Typography>{participant.pp_id} </Typography>
                </TableCell>
                <TableCell>
                  <Typography>{participant.first_name}</Typography>
                </TableCell>
                <TableCell>
                  <Typography>{participant.last_name}</Typography>
                </TableCell>
                <TableCell>
                  <Typography>Address</Typography>
                </TableCell>
                <TableCell>
                  <Typography>DOB</Typography>
                </TableCell>
                <TableCell>
                  <Fab color="primary" size="small" aria-label="add">
                    <AddIcon />
                  </Fab>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
      <Fab color="primary" aria-label="add" size="large">
        <AddIcon />
      </Fab>
    </div>
  )
}

export default Participants

Lets go thru the file line by line. We start by importing a bunch of dependencies that we need. We of course import React, and we import some React Hooks { useContext, useEffect, useState }, which I will talk about later. We also import the rootStoreContext from the RootStore, and a whole bunch of Material-UI components.

If we move down to line 14, we see the beginning of our component const Participants = () => { Here we are creating a functional React component. If you dont know the difference between functional and Class based React Components, it may be worth a read but heres my explanation in short. Functional components are an easy way of creating a React component. They were initially for components that didn't have anything to do with state, like a button. In contrast, class based components were for state management; they have access to a bunch of React extras (through extending the base React component class), things like lifecycle methods which help in managing state. But, eventually the creators of React noticed just how hard it was to access data from one Class based component in another, which is a common need in more complex applications (like ours). So, ways were devised to easily pass around data in function based components in later versions of React. Lets talk about that now.

Moving on to the next line, line 15, we see const rootStore = useContext(rootStoreContext). There are a few things going on here. First, useContext is a React Hook: useContext. React Hooks provide easy ways to hook into bits and pieces of React's API without having to drag it all in thru using a Class based component. So for example, useContext allows us to "hook into" the Context API part of React. The Context API is for creating and using "globally available" Javascript objects; things that we want to be able to use across components. From the React docs -> "Context provides a way to pass data through the component tree without having to pass props down manually at every level." So we can see that we are using the "globally available" rootStoreContext object as imported from the RootStore.

In the next line, line 16, we see const [participants, setParticipants] = useState([]). useState is another React Hook, and their docs explain it best: "They let you use state and other React features without writing a class." The useState hook sats I want to create a stately (mutable) variable on this component called participants, and I want to create a way to update that variable called setParticipants. It also allows you to pass in the initial value of the participants variable [], an empty array. If you look further down to ther return statement jsx of the function, we can see the participants variable in use. But when the component is initialized on the DOM, we will have an initial value of an empty array, so nothing would be displayed.

So our next step is to get the data that we need to display on this page and update the state with that data. We do that using another React Hook called useEffect:

    const fetchData = async () => {
      await rootStore.ParticipantStore.getParticipants()
      setParticipants(rootStore.ParticipantStore.participants)
    }
    fetchData()
  }, [])```
The `useEffect` hook gets called _after_ every render and re-render of the component.  The first parameter of useEffect is the callback method that gets called on each render/re-render:
```() => {
    const fetchData = async () => {
      await rootStore.ParticipantStore.getParticipants()
      setParticipants(rootStore.ParticipantStore.participants)
    }
    fetchData()```
Here we have a anonymous function that defines an asycronous function and calls it immediately.  The asyncronous function calls the getParticipants() function from the ParticipantStore and awaits for the response using Promises (using async/await).  Then, it uses the `setParticipants` method as created by the useState hook to update the `participants` value on the DOM with the new stored value which is in the store `rootStore.ParticipantStore.participants` (there will be more on the ParticipantStore later).  

You may be thinking if the useEffect hook runs after every render, and our callback method re-renders the component thru the `setParticipants` state update method, wont this callback run indefinitely?  Yes!  So, how do we stop that?  Thru the optional second parameter of the useEffect hook, which is there to provide some control.  Thru this parameter, we can pass in the state or prop variable(s) that needs to change to trigger the re-render.    So, lets say I only want to run this effect if there are changes to the `participants` state variable.  Then I would say `useEffect(<callback function that run after a change to participants>, [participants])`.  Anything thats in that second parameter's array will get compared to its previously rendered value, and if its different it will run the callback.  But that wouldn't solve the issue of the indefinite loop either since the participants value keeps getting updated and triggering the re-render.  The way around this is to add an empty array as the second parameter to useEffect, this way we tell React that this effect doesnt depend on _any_ state or prop values, so it never needs to be rerun after the initial render.  

And now finally, we get to the actual UI of the component itself, everything in the `return` statement.  As we see here there are a bunch of Material-UI components to create the UI of the view.  On line 65 the table body starts and looks like this: 
{participants.map((participant, index) => ( Number {participant.pp_id} {participant.first_name} {participant.last_name} Address DOB ))} ``` Here we are using React's JSX `map` method to iterate over the values of the `participants` array.

On the final line of the Participants component we export the Component itself.

Step 2 - Managing Your Component's State

Does your component have state? A way to answer this question is to ask if your component has mutable/changable data on it, or does your component have a need to interact with the database? If no, you can skip to the step on adding your component to the router. If yes, you will need to add a place to store that data, and for that we use the concept of [stores](https://mobx.js.org/best/store.html). To follow our example of the Participants component, we definitely need state because we update our participants in this component. So we need to do a few things. First we need to create a corresponding store file to store mutable data and ways to change said data called ParticipantStore; these files should be created in the src/stores folder. And second we need to add that store to our RootStore, also found in the stores folder.

The RootStore looks like this:

import { createContext } from "react"
import { AuthStore } from "./AuthStore"
import { ParticipantStore } from "./ParticipantStore"
import { QueueStore } from "./QueueStore"

export class RootStore {
  // If creating a new store dont forget to add it here.
  authStore = new AuthStore(this)
  ParticipantStore = new ParticipantStore(this)
  QueueStore = new QueueStore(this)
}

export const rootStoreContext = createContext(new RootStore())

As you can see we added the ParticipantsStore to the root store. The RootStore also uses the Context API to create a globally usable javascript object. This way all stores get instantiated together and they share reference.

Heres what the ParticipantStore file should look like:

import { observable, action, flow, toJS, decorate } from "mobx"
import { createContext } from "react"
import api from "../api"

export class ParticipantStore {
  constructor(rootStore) {
    this.rootStore = rootStore
  }

  participants = []

  setParticipants = data => {
    this.participants = data
  }

  getParticipants = flow(function*() {
    const { ok, data } = yield api.getParticipants()
    if (ok) {
      this.setParticipants(data)
    } else {
      // TODO: Handle errors
    }
  })
}

decorate(ParticipantStore, {
  participants: observable,
  setParticipants: action,
})

//let participantStore = (window.participantStore = new ParticipantStore())
export const ParticipantStoreContext = createContext(new ParticipantStore())

So first we start by importing some functionality from mobx dependency which will make it easier to handle state changes. Then I imported createContext from React (which honestly may not be necessary since the root store is already on a global context and this store is in the root store). And, I imported the api from the api file.

In the ParticipantsStore we start with a constructor method

constructor(rootStore) {
  this.rootStore = rootStore
}

The rootStore is global, as we know from looking at it; so we pass it in to the constructor method and set it as the rootStore of this ParticipantStore class.

If we look down at the decorate function below the ParticipantStore, we see this:

decorate(ParticipantStore, {
  participants: observable,
  setParticipants: action,
})

What this is saying is that in the ParticipantStore, we want to add the observable mobx decorator to the particpants variable, and the action mobx decorator to the setParticipants function. (We do it this way because the es linters hates adding them directly like @observable participants = [].)

So back in the ParticipantStore, we have the observable participants array where we want to store our participants. We marked it as observable meaning that a setParticipants method and a getParticipants generator function (ES6 feature function* & yield)

Step 3 - Create the API File

Heres the ParticipantAPI file:

import createAuthRefreshInterceptor from "axios-auth-refresh"
import refreshAuthLogic from "./refreshAuthLogic"

const create = () => {
  const accessToken = localStorage.getItem("JWT_ACCESS")

  const api = apisauce.create({
    baseURL: "/api",
    headers: { Authorization: `Bearer ${accessToken}` },
  })

  createAuthRefreshInterceptor(api.axiosInstance, refreshAuthLogic(api))

  const getParticipants = async () => {
    const response = await api.get("/participants/")
    return response
  }
  return {
    getParticipants,
  }
}

export default create()

All it is doing right now is creating a request to get all the participants from the database.

now that we have added our participant store. The root store is already provided to the App, so all of our stores in the Root Store, are automatically usable on the DOM.

Step 4 - testing

Testing is not really needed, although I think its a good idea to see if you are actually on the right track here. You may have noticed in the Participants Store the 2 lines that say this: // uncomment this line to have the store on the dom and testable // var store = (window.store = new ParticipantStore()) If you uncomment the var store line, you can now use store in your browser's dev console for testing. This is a great way to see if your requests are actually working yet. So for example, if you open up your App in your browser, open up up your dev tool console, and type store you should get back the ParticipantStore. And if you type store.getParticipant() you can actually hit that endpoint against your local backend and get all the participants from your local db (if its up and running). This way you can test your requests.

I also would suggest using Postman, its a helpful tool for creating the API requests without having to worry about all this other stuff first.

Step 5 - Routing to the right URLs

The next step is getting the components on the right urls of the App. Now that we have the store and api up and running, lets display our component. The src/routes folder is where we will do this work. In the index.js file we can add our private route to the Router like so:

import Navbar from "../components/Navbar"
import LoginForm from "../components/LoginForm"
import ParticipantSearch from "../components/ParticipantSearch"
import Participants from "../components/Participant"
import { BrowserRouter as Router, Route } from "react-router-dom"
import PrivateRoute from "../routes/PrivateRoute"

const Routes = () => {
  return (
    <Router>
      <Navbar />
      <PrivateRoute exact path="/" component={ParticipantSearch} />
      <PrivateRoute exact path="/participants" component={Participants} />
      <Route path="/login" component={LoginForm} />
    </Router>
  )
}

export default Routes

We added the line: <PrivateRoute exact path="/participants" component={Participants} /> we marked it as a private route since it displays data that a user with credentials needs access to. If you look at the PrivateRoute.js file, you will see what I mean, it takes you back to the login screen if not authenticated. Now if you go to your browser and go to the /participants url you should see your component.

Step 6 - Styling and Displaying your Data

This section is currently a work in progress and I will be completing it soon... Here is where we can now start using Material-UI react components and or whatever styling components we want to build out how the data is displaying on the front end for the user.