DEV Community

Vikram Chatterjee
Vikram Chatterjee

Posted on

Phase Five Project - TrainingBuddy (Vikram Chatterjee, Flatiron - Software Engineering)

TrainingBuddy is a React/Redux application with a RAILS API backend that allows a user to keep track of workout sessions and each workout that is a part of that session, their personal bests, and people who inspire them to keep training. The application uses "Thunk middleware" so that asynchronous requests can be made between the Redux store and the backend API. Users can create, view and delete data from the backend using forms, links and buttons that dispatch actions to the store. The application was styled using the "MaterialUI library", in order to make the application more aesthetically pleasing than a regular HTML application.

Rails Backend
A "Rails API" backend was created for this project using 'rails new phase_five_backend --api --no-test-framework'. The schema consists of sessions; which have a name and a date, and workouts; which have a name, a number of sets, a number of reps, an integer for weight, and a session_id attribute which is used for associating the workout with a specific session. The schema also has inspirations which contains string attributes for name and image and a text attribute for bio, and bests which represents personal bests, which has an attribute of name, weight, and reps. In "routes.rb" there are resources for inspirations, bests, sessions and workouts and the workouts resource is nested within the sessions resource so that the route looks like /sessions/sessionid/workouts/workoutid which corresponds with the 'has_many/belongs_to' relationship between sessions and workouts. Serializers were set up for Sessions and Workouts so that when the API is accessed, the 'has_many/belongs_to' relationship between sessions and workouts is plainly rendered on the sessions route.

Setting up the React App with Dependencies
In order to set up our react app, we 'cd' into the 'phase_five_project' folder in the terminal and run the command 'create-react-app phase_five_frontend' which generates the initial scaffolding for the project with files such as 'Index.js' and 'App.js', which are the first and second highest level components of our application, respectively. Then we need to add dependencies using the command 'yarn add redux react-redux redux-thunk redux-devtools-extension' and 'yarn add react-router-dom'.

Creating the Store
The Redux store was created in "Index.js", the top level parent of the components in the application. Imports like 'Provider', 'createStore', 'applyMiddleware', 'compose', and 'thunk' allow for a Redux store that can communicate with the backend using asynchronous fetch requests. A variable, 'composeEnhancers', is instantiated just below the imports. This variable allows us to use the 'Redux Devtools' extension that we installed above. Another variable, 'store' is instantiated below, which calls the 'createStore' function which takes in the 'rootReducer' and the 'composeEnhancers' variable as arguments. 'composeEnhancers' takes 'applyMiddleware' as an argument, which takes in 'thunk' as an argument in turn. Then the store is passed in the Provider component so that child components of index.js can access the Redux store. Underneath the Provider component, the Router component is rendered so that any child components can access the Router which will allow for client-side routing of the different pages in the application. The parent component of the application, App, is imported and rendered beneath Router and will provide our application with all of the necessary components, as shown in figure 1. figure 1. Figure 1 - index.js

React Containers and Routes
Inside of 'App.js', we import all of our container components which in turn, contain all of our routes so that a user can see RESTful urls which correspond to each of the Component based views that are contained in the application. Nested within the App div, the Nav component is rendered, which renders a Nav bar at the top of the application page and will be visible regardless of which route the user is viewing. The Nav bar contains links for most of the pages in the application and allows the user to quickly navigate to the desired route depending on which records they would like to view or create. At the root level of the application, the Home component is rendered with a route. The SessionsContainer, BestsContainer, and InspirationsContainer are rendered underneath the switch statement containing the root path, and these containers each contain respective routes that the user can navigate to view or create records. The SessionsContainer is an example for our different containers, and is a class component that uses mapStateToProps and imports an action creator, fetchSessions, which uses an asynchronous fetch request to get our session data from the backend, as shown in Figure 2.
figure 2
Figure 2 - fetchSessions

The SessionsContainer component itself is shown in Figure 3.
figure 3
Figure 3 - SessionsContainer

When the component mounts, the 'fetchSessions' action is called and brings all of the session data from the backend into the Redux store with the help of the reducer. After all of the session data is fetched, the reducer sets the state of the sessions object within the store to the payload of the 'fetchSessions' action, which contains our session objects. Inside of the Switch statement in the 'SessionsContainer', there are three Routes that route the user to the new Session form, the show page of a session, and the index page listing the sessions respectively. It is important that /sessions/new is rendered before /sessions/:id because /:id is a dynamic route which would accept 'new' as a parameter of the URL, so if /sessions/id were rendered first, the router would attempt to navigate to a session with an id of 'new' if someone attempted to navigate to the 'new' route. In the 'new' route, no props need to be passed down from the sessions container to the new form, so we render that route with the 'component' keyword and pass the 'SessionInput' component in using JSX tags. In the /sessions/:id route, we are passing props down from the "SessionsContainer". First we pass in the 'routerProps', which include 'match' which will allow us to get the id of the specific session that we are navigating to. We also pass down the sessions prop which is accessed in the container using 'mapStateToProps'. This contains an array of objects containing data for each session.

Viewing Records
Earlier in the blog post, it was discussed that in the show and index pages of Sessions, we are passing down the routerProps and the sessions prop which contains data for each of our sessions. In the Sessions (index) component, props are passed in and in the return portion of the component, the array of sessions is mapped over using 'props.sessions.map' and a link to each session's route is rendered with the text being equal to the name of the session. A delete button is rendered next to that link, which, when clicked, will dispatch the 'deleteSession' action which will make a fetch request to update the backend, and then be passed to the reducer to remove that session object from the redux store in the frontend.
The Session component is responsible for showing an individual session to the user. In order to access a specific session object, the filter function is called on 'props.sessions' and filters through the array of session objects to find the session id which matches the id param in 'props.match.params.id' which is passed down from routerProps when a specific Id is passed into the URL as shown in Figure 4.
figure 4
Figure 4 - Session.js

The filter method contains an array of our objects which match the 'params', so we must place a [0] after the filter function in order to access the first (and only) session object which matches the filter query. The result of the filter function is set to the variable 'session' and if a session is found, the session name and date will be rendered within 'h2' tags. Below that, we render the 'WorkoutsContainer', which contains a form to enter a new workout as well as a list of workouts that are associated with that session. The session prop is passed into the 'WorkoutsContainer' so that the workouts container only submits and renders workouts that are associated with a given session. The 'WorkoutsContainer' contains two components, 'WorkoutInput' and 'Workouts', which display the form to input a new workout, and render the workouts associated with a session, respectively. 'WorkoutInput' takes props of a session in order to connect the workout with a given session ID, and 'Workouts' takes in the props of a session and the props of a workout associated with the session so that it can iterate over and display each workout associated with that session.

Adding records to the store and backend
This application uses controlled forms and calls on action creators in order to update the backend and the Redux store. In the 'sessionInput' component, the state is an object containing two key value pairs, 'name' and 'date'. Both are initially set to empty strings. In the render function, a return value is set to a form with 'TextFields' for the session name and date. The values are set to 'this.state.name' and 'this.state.date' respectively. An 'onChange' parameter invokes the 'handleChange' function which uses 'setState' to modify the internal state of the component. The form has an 'onSubmit' parameter which calls on the 'handleSubmit' function. The 'handleSubmit' function prevents the default refresh which would happen when a submit button is clicked, and then calls on the action creator 'addSession' which takes in the component's state and makes a fetch request to the backend and then dispatches an action of type ADD_SESSION to the reducer which contains a payload of the session object to be added to the Redux store's state as shown in Figure 5.
figure 5 Figure 5 - sessionsReducer.js

Another example of a controlled form within this project resides within the 'WorkoutInput' component, which uses 'handleChange' and 'handleSubmit' in a similar manner to the 'SessionInput' component, but contains a state and form which contain the name, sets, reps, and weight of a workout. It is important to note that we have to import the action creators associated with these components, and 'export default connect' the components at the bottom, passing in the action creator as the second argument in the connect function. One difference between the 'addSession' action creator and the 'addWorkout' action creator is that 'addWorkout' takes two arguments: the internal state of the component and the id of the session associated with the workout. The 'addWorkout' function uses the 'sessionId' argument as part of the url of the fetch request, so that the program knows where to go to add the workout to a given session. The action that is dispatched to the reducer actually contains a payload of that workout's associated session, so that the store is updated with the version of the session containing a new workout.

Deleting Records
For each of the components that render data from our models, there is a delete button beside the name of each object that is rendered. For example, in the Workouts component, we have a button which, when clicked, triggers the 'handleDelete' function with the workout object associated with that list item being passed in as an argument to 'handleDelete'. We import the action creator 'deleteWorkout', and we pass it into the second argument of the connect function at the bottom, which makes the function available as a prop in the component. Inside the 'handleDelete' function, we call 'props.deleteWorkout' and pass in the id of the workout and the id of its associated session so that we can target the workout and modify the associated session in the Redux store. In the fetch request, we interpolate the session id and the workout id in the URL so that we are deleting data from the correct route. When the delete request is finished, we take the session object and dispatch an action to the reducer which maps over the sessions in the store's state and returns the new session which excludes the workout which was just deleted. In the sessions component, we have a similar 'handleDelete' function which in this case only takes the 'sessionId', makes the delete request, and passes the session object into the payload of the dispatch of type DELETE_SESSION, but this time, in the reducer, we use a filter function to return all of the sessions whose id does not match the session passed into the payload of the dispatch object, which modifies the state to exclude the session that was deleted.

In this blog post, examples from the sessions and workouts components have been given, but the 'inspirations' and 'bests' components work in very similar ways. A summary of the Rails backend, an explanation of store creation, routes and containers, and adding and removing data from the Redux store and backend have been given. To view the project files, go to https://github.com/Strycora/phase_five_frontend to view the frontend files, and https://github.com/Strycora/phase_five_backend to view backend files.

TrainingBuddy is a great resource for active people who want to log their sessions, workouts, and personal bests! As a bonus, users are able to document those people who inspire them to pursue fitness.

Top comments (0)