Skip to content

Creating Front End Components

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

Introduction

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 over throughout the app. Eg, the navigation bar component is on each screen, or a button component will also be re-used several times over. Some components we use come 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 "statefull" component, one that has a way to manage the state. We have chosen to use React Hooks and Mobx for our state management. Each statefull 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 statefull 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"
import { observer } from "mobx-react-lite"

const ParticipantsList = observer(() => {
  const rootStore = useContext(rootStoreContext)
  const participantsStore = rootStore.ParticipantStore

  // useEffect is a hook that gets called after every render/re-render.  Empty array second argument prevents it from running again.
  useEffect(() => {
    const fetchData = async () => {
      await participantsStore.getParticipants()
    }
    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>
            {participantsStore.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>
        <p>{participantsStore.filter("T9FN3", null, null).first_name}</p>
      </div>
    </div>
  )
})

export default ParticipantsList

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 }, 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 ParticipantsList = observer(() => {

Here we are creating a functional React component and using a Mobx observer decorator to wrap our component. This decorator is connected to our participants store's data. We use it for storing state data. Here's a helpful link on understanding using mobx with our React component.

Tangent on functional vs class based components

If you don't know the difference between functional and Class based React Components, it may be worth a read but here's 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. So usually Class based components would be higher order components or container components that would house all the state change logic as well as passing it to the children components. But, eventually the creators of React noticed just how hard it was to access data from one Class based component in another Class based component, 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.

React Hooks and Mobx Stores

Moving on to the next lines, we see const rootStore = useContext(rootStoreContext) and const participantsStore = rootStore.ParticipantStore. 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. The stores are where Mobx comes into play. We use Mobx's helper decorators in the store and in the component to easily deal with state.

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 a React Hook called useEffect:

    const fetchData = async () => {
      await participantStore.getParticipants()
    }
    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 participantStore.getParticipants()
    }
    fetchData()

and the second parameter, which is in this case just an empty array, is an optional one that provides some control over when re-rendering happens.

With the first param, we have an anonymous asycronous function definition and immediate call. The asyncronous function waits for the returned value from the getParticipants() function from the participantStore. In the store that function actually puts the return value into an array called participants (see Step 3).

With the optional second param of the useEffect hook, we can pass in the state or prop variable(s) that needs to change in order to trigger the re-render. So if we only want to run this effect if there are changes to the participants state variable, then I would say useEffect(<callback function>, [participants]). Anything that's in that second parameter's array will get compared to its previously rendered value, and if its different it will run the callback. So, we passed in an empty array. Why not pass nothing if it is an optional param? Well, if we past nothing then we would run the risk of having an indefinitely rendering object because a re-render would always trigger the useEffect to be called which would trigger a re-render. The way around that is to add an empty array as the second parameter to useEffect. Buy doing so, we tell React that this effect doesn't 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:

<TableBody>
  {participantsStore.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>

Here we are using React's JSX [map](https://reactjs.org/docs/lists-and-keys.html) method to iterate over the values of the participants array that's in the participantsStore.

On the final line of the ParticipantsList component we export the Component itself. This is the basic component, you can definitely create smaller components within, such as a Participant component to have for each line of the Table, which will make your code more module.

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/changeable 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 apisauce from "apisauce"
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.

Step 4 - Testing

All components need Unit and Functional tests. See the wiki doc on creating tests here.

Here are some other helpful things. You may have noticed in the Participants Store this commented out line at the bottom:

//let participantStore = (window.participantStore = new ParticipantStore())

If you uncomment the line, you can now use participantStore on your browser's dev console for testing. This is a great way to see if your API requests are actually working. So for example, if you open up your App in your browser, open up up your dev tool console, and type participantStore you should get back the ParticipantStore. And if you type participantStore.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/ParticipantsList"
import { BrowserRouter as Router, Route } from "react-router-dom"
import PrivateRoute from "../routes/PrivateRoute"

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

export default Routes

We added the line:

<PrivateRoute exact path="/participants" component={ParticipantsList} />

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

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.