Learn How to Build a Full-Stack Application Using Node.Js and React in 2021 — Part Two (Front End)

Eli Sultanov
Geek Culture
Published in
19 min readMar 15, 2021

--

Photo by James Harrison on Unsplash

Part one can be found here: PART ONE: Back End

*Sorry for taking forever to write part 2, got busy with life and school.*

Let’s recap: In part one, we started by organizing our backend and installing a few packages for both the frontend and the backend.

We have also created a few API endpoints that made up our crud application. In part one, we also connected our MongoDB database to our application and tested its usability using postman.

STEP 1: Frontend folder/file structure

Inside our client, head over to the src folder and in there create the following folders:

api,

_actions,

_reducers,

components

At this point, our client folder should look something like this.

STEP 2 : Connecting our backend and the frontend

Inside the api folder create a file called index.js.

The first thing we need to do inside our index.js file is import axios.

import axios from 'axios';

Now, let’s create a new variable called url and assign our localhost:8000 address to it.

const url = 'http://localhost:8000/posts';

Let’s get a little more interesting here.

We’ll create four functions, and each function will represent a backend call.

The first function will trigger the create operation.

export const createPost = (post) => axios.post(url, post);

Let’s break this a little further and understand what is happening here.

The export.

In JavaScript, there are two types of exports, named exports and default exports. The above function relies on named exports.

Named exports are useful when a file contains a few functions that need to be exported, in the case of our index.js file. Wherewith a default export, we will be exporting a single function.

The arrow function.

Here we are using the shorthand version of the arrow function, and our arrow function will accept a single argument, the post that is being sent when the form is submitted.

axios

axios is a js library that helps us with sending and receiving requests to and from our backend.

Most of the functions that are provided to us by axios are async, so at some point, we will need to use async/await, but we won’t use it here, rather we will use it in the _actions folder.

axios.post takes in two arguments, a url and what it should send to the backend.

The next crud operation that we will set up will be the read or get.

export const getPosts = () => axios.get(url);

Here we are simply telling axios to send a get request to the backend.

The next two operations are update and delete. Let’s deal with update.

export const updatePost = (id, post) => axios.patch(`${url}/${id}`, post);

Our updatePost function takes in two arguments, id, and post. Once we have the id, we tell axios to make a patch request to the given id. This action will send the new contents of the post to the backend.

Lastly, the delete

export const deletePost = (id) => axios.delete(`${url}/${id}`);

Our deletePost function will only take in a single argument, id. Once we got the id, we will tell axios to make a delete request to the particular id.

Our index.js file should look similar to this.

STEP 3: Let’s talk redux for few mins.

In part one, I mentioned Redux briefly, but now It’s time that we formally introduce Redux and why it is needed. Keep in mind that Redux isn’t required at all, but it’s a good idea to use it. See, with react, we have a concept such as props passing.
With simple applications, passing props around is totally fine and probably a better choice over Redux, but when we start to deal with APIs and building components that are deeply nested, passing props gets complicated, and we tend to make more errors that cause our application to crash more frequently.

Redux has four main concepts, actions, reducers, dispatch, and store.

Actions are like messages that we want to send. Those messages are usually objects. Our messages need two things, a type, and payload. Type lets Redux know who we are sending the message to, while the payload is the message itself. In the example code, we use action creators that are simple functions that return our action.

Think of a reducer like it’s a storage container. Our reducer is a function that takes in two arguments, the initial state (it can either be an array, an object, a Number, or a String, though it is recommended to use either an object or an array), and the action. Based on the action type, we can determine what we want to do with what’s in the state.

Dispatch is used for dispatching our action.

The store is what holds everything together. Our store takes in the reducer and based on what dispatch gets from actions. The store tells our reducer what to do.

Let’s see a simple example of redux in action.

import {createStore} from 'redux'// reducerconst reducer = (state=[], action) => {switch(action.type) {case "SAY": 

return [...state, action.payload]

default:
return state;}// storeconst store = createStore(reducer);// action creatorfunction sayHello = {type: "SAY",payload: "Hello"}// dispatchstore.dispatch(sayHello());}

Let’s analyze this piece of code. First, we need to import our createStore. Afterward, we create our reducer, which will have a state of an empty array and action.

Inside our reducer we have a switch statement that will determine which case needs to be activated based on action.type.

We assign the reducer to the store.

We have an action creator, which is a simple function that returns an object with type and payload.

And lastly we have dispatch that will trigger our action creator.

If you want to learn more about redux, definitely check out the official docs.

side note: Our code will be slightly different than the example code because we will be using react-redux and thunk.

STEP 4: Creating some actions.

Since we are using redux in our project, it wise to start with actions. Let’s make a new file inside _actions called posts.js.

Our actions will go hand and hand with our api. Let’s start by first importing our API.

import * as api from '../api/index';

The star(*) tells our application that we want to import everything from our index.js file, and the new name will be api.

Let’s create our first function that will be a curried function. Curried functions are a little tricky because we are now dealing with nested functions. In our case, we have a createPost function that accepts a single argument, post. Then we are nesting our second function inside createPost, which is an async function that accepts a single argument, dispatch.

export const createPost = (post) => async (dispatch) => {const { data } = await api.createPost(post);dispatch({ type: 'POST', payload: data });}

Now that this is out of the way. Let’s dig deeper. Our function will do two things. First, it will need to destructure data from our api. Destructuring is very useful because we are retrieving specific information and assigning it all at the same time.

The second thing we are doing is setting up our dispatch to execute once our createPost function is called elsewhere.

side note: To learn more curried function, checkout this stackoverflow question.

To learn more about destructuring, check out this guide by dmitripavlutin.

The next function will focus on getting every post that we have created so far,

export const getPosts = () => async (dispatch) => {const {data} = await api.getPosts();dispatch({ type: 'GET', payload: data})}

As you can see, we have a pattern here. The only things that really change are our API call and the type.

The last two function will be update and delete.

export const updatePost = (id,post) => async (dispatch) => {const {data} = await api.updatePost(id,post);dispatch({ type: 'UPDATE', payload: id})}export const deletePost = (id) => async (dispatch) => {const { data } = await api.deletePost(id);dispatch({ type: 'DELETE', payload: data });}

Both update and delete require us to have an id, and we will get that id later on.

Ok, now we are done creating our actions. Time to move to our reducer.

STEP 5: Creating our reducer.

Inside the reducer, folder create two files, index.js, and posts.js.

First, we gonna work on the posts.js file and then work on index.js.

Let’s start by creating a simple function

export default (posts=[], action) => {}

Remember how I mentioned named and default exports? Here we are using a default export. From here, it’s just a basic function that takes in two arguments, default state, and action.

Now, let’s add a switch statement.

export default (posts = [], action) => {switch (action.type) {}}

Our switch statement will watch for what it’ll get from action.type. Remember in step 4 our action and dispatch? Yea, here is the connection, action.type gets whatever dispatch({type:’something’}) gives it.

Pretty standard so far.

Let’s add a default case and a first case.

export default (posts = [], action) => {switch (action.type) {case 'POST':return [...posts, action.payload];default:return posts;}}

With redux, we always need to return some sort of a state, and if a case is not provided or there is a typo, the default case kicks in and returns the unchanged state. Always have a default case that returns your unchanged state. This will save you a headache.

Ok, our first case, what’s happening there.

When we want to add something to the array, we usually use the push method, but redux doesn’t recommend using push because it is considered mutable, or in plain English, it will change our array.

In redux, we never want to modify our state, but rather, we want to change it with a completely new array that has our new item. This concept is a little tricky to wrap your head around, and it took me a while to understand it. I highly recommend you guys to read an article by Toptal.

Ok, what are the three dots you may ask? Well, it’s magic, seriously. They’re called a spread operator. What’s happening is that the three dots clone our state, and add the payload to the end, so now we have a new array called posts that includes our newly added item. I think you might enjoy reading this StackOverflow answer.

Let’s finish the remaining cases.

export default (posts = [], action) => {switch (action.type) {case 'POST':return [...posts, action.payload];case 'GET':return action.payload;case 'UPDATE':return posts.map(post => post._id === action.payload._id ?   action.payload : post);case 'DELETE':return posts.filter(post => post._id !== action.payload);default:return posts;}}

We added our get, update, and delete. Our get case returns our payload. For our update, we use map to loop through our array and find a post that matches the id, and if we find a match, we replace it with our payload. If no match is found, we leave post as-is. Lastly, we delete using filter.

Both map and filter return new arrays, so they are considered immutable.

Before we can start working with react, let’s head over to index.js file that is located inside the reducer folder and add a few lines in there.

import { combineReducers } from 'redux';import posts from './posts';export default combineReducers({posts})

A quick run-through. We are importing combineReducers from redux. combineReducers help us with dealing with multiple reducers. We import our posts reducer and export it through combineReducers.

We are officially done with the reducer part. Let’s start working with react.

STEP 6: Some react clean up

Delete Every file from the src folder that isn’t part of the folders we have created. The only two files that should survive this purge are App.js and index.js.

Your src folder should look like this:

Let’s continue with our clean up.

Head over to the main index.js file and paste the following:

import React from 'react';import ReactDOM from 'react-dom';import App from './App';ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'));

We removed a lot of unnecessary lines for this particular project,

Now, open App.js and delete everything and paste the following.

import React from 'react'function App() {return (<></>);}export default App;

The reason why I did this mini purge was that I like to work with a clean slate.

STEP 7: Let’s create some components.

Inside the components folder, create two new folders, form and posts.

Inside the form folder, create a Form.js file, make sure the f is capitalized, it’ll tell react it’s dealing with a component.

inside Form.js file paste the following code.

import React from 'react'import { useForm } from 'react-hook-form';export default function Form() {const { register, handleSubmit } = useForm();return (<>
<form>
<div><input type="text" name="author" placeholder="Author" {...register('author')} /></div><div><input type="text" name="title" placeholder="Title" {...register('title')} /></div><div><input type="text" name="article" placeholder="article" {...register('article')} /></div><button type="submit">Submit</button></form></>)}

First, we import react and the useForm hook that will help us with dealing with forms. Afterward, we create a simple react component and add a simple HTML form to it with a few input fields. ref={register} is needed because it tells react-hook-form to look at the field capture whatever the user types into it, all without us having to keep track of changes. HUGE TIME SAVER.

Head over to App.js and import the Form component. Your App.js should now look like this.

import React from 'react'import Form from './components/form/Form';function App() {return (<><Form /></>);}export default App;

To check out our application in the web browser, make sure your backend and react are both running.

Quick reminder: inside the server folder, run nodemon app.js inside the client folder run npm start.

At this point, we should have a beautiful form, should look something this:

Clearly we are building the next Facebook here.

STEP 8: Let’s connect redux with react

Open the main index.js file and paste the following:

import React from 'react';import ReactDOM from 'react-dom';import App from './App';import { Provider } from 'react-redux';import { createStore, applyMiddleware, compose } from 'redux';import Thunk from 'redux-thunk';import reducer from './_reducers/index';const store = createStore(reducer, compose(applyMiddleware(Thunk)))ReactDOM.render(<React.StrictMode><Provider store={store}><App /></Provider></React.StrictMode>,document.getElementById('root'));

Ok, let’s understand what we did here. We imported Provider, createStore, applyMiddleware, compose, Thunk and store.

The provider is offered to us by react-redux, and its entire job is to make our lives easier when working react and redux at the same time.

CreateStore is pretty self-explanatory. It creates a store for us.

Compose lets us to work with more than one redux enhancer. applyMiddleware is an enhancer that lets us work middleware, and Thunk is a middleware that we are planning to use.

Remember earlier how we used curried functions? That’s possible in redux thanks to Thunk.

STEP 9: Let’s continue building out components

Head over to App.js and add the following:

import React, { useEffect } from 'react'import Form from './components/form/Form';import { useDispatch } from 'react-redux';import { getPosts } from './_actions/posts';function App() {const dispatch = useDispatch();useEffect(() => {dispatch(getPosts);}, [dispatch])return (<><Form /></>);}export default App;

We added a few things to our App.js to make our application better. First, we imported useEffect, then we imported useDispatch, and lastly, we imported our getPosts actions.

useEffect hook is pretty cool because it lets us get all of the posted from our database as soon as the user visits the website.

The way useEffect works is that when someone visits a page that has a useEffect hook, the data will be available on the first load. The [] tell useEffect to only update when a particular item inside the [] changes, and in our case, its dispatch.

STEP 10 : Let’s continue working on our Form.js file

Head back to the form component and let’s make it do some action, for example, send data to our database.

Our new Form.js file should look like this:

import React from 'react';import { useForm } from 'react-hook-form';import { createPost } from '../../_actions/posts';import { useDispatch } from 'react-redux';export default function Form() {const { register, handleSubmit, reset } = useForm();const dispatch = useDispatch()const onSubmit = (data) => {dispatch(createPost(data))reset()}return (<><form onSubmit={handleSubmit(onSubmit)}><div><input type="text" name="author" placeholder="Author" {...register('author')} /></div><div><input type="text" name="title" placeholder="Title" {...register('title')}/></div><div><input type="text" name="article" placeholder="article" {...register('article')} /></div><button type="submit">Submit</button></form></>)}

When comparing our old Form.js file with the updated version, a few things should catch our eye.

First, we got a few new import lines that we should be familiar with by now.

import { createPost } from '../../_actions/posts';import { useDispatch } from 'react-redux';

The first import gets our action for us, and the second import gets us the dispatch hook. Pretty self-explanatory.

The next new line is our useDispatch hook being used.

const dispatch = useDispatch();

We have another new thing in our Form.js file it’s a function called onSubmit. This single function makes forms usable in react..

Let’s dissect this small function.

const onSubmit = (data) => {dispatch(createPost(data))reset()}

Our onSubmit function takes a single argument. That argument will be called data.

Inside our onSubmit, we will do two things, dispatch an action that will create a new post inside our database, and clear the input fields.

How will it know how to clear the fields and where is the data coming from, you may ask?

The field clearing is very easy, just add another key to our useForm hook called reset.

const { register, handleSubmit, reset } = useForm();

Now, our data is coming from our input fields through ref={register}. It is passed on to our handleSubmit function that accepts our onSubmit function. It’s kinda confusing, but it works.

In short, when a user submits the form, a few things happen. First, we get all of the values and names of our fields that live inside register. Our register function then passes all of the fields to the handleSubmit function as data that our onSubmit function gets. Pre-lastly, the data we now have is passed to our createPost action that tells our backend to create a new post inside our database, and lastly, as soon as we are done, the reset function is executed, and our fields are cleared

I highly recommend that you read react-hook-form api because it has some valuable information, and quite frankly, it is one of the best documentation I’ve come across in a little while.

STEP 11: Let’s display some data that we submitted

If you followed this tutorial this far, you should have a working form that submits data to the database. It’s time to get our data back and show it to the world.

Inside our components folder, we should have a folder called posts, and if you don’t that folder, create it.

Inside our posts folder, create a file called Posts.js, also, create another folder called post. Inside the post, folder create a file called Post.js.

This is done for the sake of organization, don’t look at me like that.

Let’s start coding, again.

Open your Post.js file, make sure that it is in that post folder. Inside the Post.js file, create a new react component.

Our Post component will accept a few props such as author, title, article, and createAt.

Post.js should look this this:

import React from 'react'export default function Post({author, title, article, createAt}) {return (<></>)}

Great, let’s populate our component with a div, an h1 that will be used for the title, and an h3 that will be used for the article, a p tag for the author, and a span tag that will hold our date.

At this point, our Post.js file should look similar to this:

import React from 'react'export default function Post({author, title, article, createAt}) {return (<><div><p>Author: {author}</p><h1>{title}</h1><h3>{article}</h3><span>{createAt}</span></div></>)}

We are done with our Post component. time to move onto our Posts component.

STEP 11.5 : Continuing….

Inside of Posts.js, import react and set it up using the previous template of Post.js(the one that doesn’t have our HTML tags).

Before we continue, install dayjs using npm i dayjs. Dayjs is a JavaScript library that helps us with displaying dates.

Add the following imports to your Posts.js file:

import { useSelector } from 'react-redux';import Post from './post/Post';import * as dayjs from 'dayjs';import relativeTime from 'dayjs/plugin/relativeTime';dayjs.extend(relativeTime);

I didn’t include the React import because I assume you already have it.

Let’s dive deep into the new imports.

First we have the useSelector.

import { useSelector } from 'react-redux';

UseSelector helps us with retrieving our data (state) from a particular reducer, and in our case, this reducer is posts.

Next in line we have dayjs.

import * as dayjs from 'dayjs';

As mentioned earlier, dayjs is a JavaScript library that helps us with displaying dates that a human can understand. FYI, dayjs does more than displaying dates. To learn more about this library, read their documentation.

The last two lines are pretty self explanatory. The relativeTime import imports a plugin that dayjs using extends can use to display relative dates.

Time to use our imports.

The first import we will use is our useSelector hook. The way it works is that it accepts a callback function that returns our state. Afterward, we assign whatever it returns to a variable.

Something like this:

const posts = useSelector(state => state.posts)

Our posts variable now holds an array because our state was an array.

At this point, we are simply going to loop through our posts and populate the Post component, and for the loop, we will use map.

This should be the end product.

{posts.map(post => {return (<Post key={post._id} author={post.author} title={post.title} article={post.article} createAt={dayjs(post.createdAt).format('MM/DD/YYYY')}/>)})}

Let’s chop this into chunks that make sense.

First, we loop through our posts variable using map. Each array index holds a single post object that in turn holds our author, title, article, and created date. We then take this single object and pass it to our Post component as individual props.

Folks, we are half way done, we have a half baked app that adds and shows data from the database. Time to add the remaining features such as update and delete.

STEP 12 : Updating our data

Folks, we are halfway done with our half-baked app that now adds and shows data from the database. It is Time to add the remaining features such as update and delete.

We’ll begin with App.js. Inside App.js, import useState hook and assign a state to it, 0 will be fine.

Your import statement should look like this:

import React, {useState, useEffect } from 'react'

And your useState should look like this:

const [id, setId] = useState(0)

Let’s update our App.js file a little further. Update the following:

<Form setId={setId} getId={id}/><Posts setId={setId}/>

What we have done above was simply pass our state(id) and gave Posts and Form the ability to change the state for us.

Navigate to your Posts.js component and let’s give it a few props.

Update the following:

export default function Posts() {

To this:

export default function Posts({setId}) {

Also let’s update our

Our loop from this:

{posts.map(post => {return (<Post key={post._id} author={post.author} title={post.title} article={post.article} createAt={dayjs(post.createdAt).format('MM/DD/YYYY')}/>)})}

To this:

{posts.map(post => {return (<Post key={post._id} setId={setId} postId={post._id} author={post.author} title={post.title} article={post.article} createAt={dayjs(post.createdAt).format('MM/DD/YYYY')}/>)})}

We’ve passed the setId prop to the Post.js component, as well as passing the individual post id to the postId prop.

Now, open your Post.js component and add two HTML buttons, one will be used for updating, and the other one will be used for deleting. Also, pass the props that we added such as, the setId and postId.

Both buttons will deal with onClick, so add that as well. Your update button should like this:

<button onClick={() => {setId(postId)}}>Update</button>

We will work on the delete button later.

Head to Form.js component, and let’s add some stuff to it.

First, we will need to import useEffect. We will also need to import useSelector, and don’t forget to import our updatePost action.

Also, pass setId and getId to Form.

Inside your useForm, add another property called setValue. setValue helps us with capturing the updated data when a user clicks update.

Below your useForm hook add the following:

const post = useSelector(state => state.posts.find((post) => post._id === getId ? post: null ))

What useSelector is doing here is that it’s finding a single post. If it finds a post, it’ll assign it to our post variable. If it doesn’t, it’ll pass null to our post variable.

Let’s use useEffect. Add the following piece of code:

useEffect(() => {if (getId !== 0) {let keys = Object.keys(post);keys.forEach((key) => setValue(key, post[key]))}}, [getId, post ,setValue])

The cool thing here is that useEffect will only execute if we have an id. If we don’t, nothing will happen. But if we have an id, we will first need to find all of the keys such as author, title, article, and created date. Afterward, we will use forEach to loop through each key and use setValue that we got from useForm to assign values to our input fields.

Another thing to pay attention to is the items inside the square brackets. Their job is to tell useEffect to execute only when one of them changes.

Lastly, let’s update our onSubmit function. Our new onSubmit should look like this:

const onSubmit = (data) => {if (getId === 0) {dispatch(createPost(data))reset()} else {dispatch(updatePost(getId, data))reset()setId(0)}}

We used conditional statements to check whether have an id or not. If we have an id, updatePost will be dispatched. If we don’t have an id, createPost will be dispatched.

Our final Form.js should look like this:

import React, {useEffect} from 'react';import { useForm } from 'react-hook-form';import { createPost, updatePost } from '../../_actions/posts';import { useDispatch } from 'react-redux';import {useSelector} from 'react-redux'export default function Form({setId, getId}) {const { register, handleSubmit, reset, setValue } = useForm();const post = useSelector(state => state.posts.find((post) => post._id === getId ? post: null ))const dispatch = useDispatch()useEffect(() => {if (getId !== 0) {let keys = Object.keys(post);keys.forEach((key) => setValue(key, post[key]))}}, [getId, post, setValue])const onSubmit = (data) => {if (getId === 0) {dispatch(createPost(data))reset()} else {dispatch(updatePost(getId, data))reset()
setId(0)
}}return (<><form onSubmit={handleSubmit(onSubmit)}><div><input type="text" name="author" placeholder="Author" {...register('author')} /></div><div><input type="text" name="title" placeholder="Title" {...register('title')} /></div><div><input type="text" name="article" placeholder="article" {...register('article')} /></div><button type="submit">Submit</button></form></>)}

Well, folks, I just want to say that all the hard work is nearly done! The last thing we want to include is the delete part of our crud application.

STEP 13: The delete button

Remember how I mentioned that we will return to the delete button, it’s time.

Open your Post.js component and import useDispatch as well as importing our deletePost action.

Inside your delete button, add an onClick prop and tell it to dispatch the deletePost action.

Your delete button should look like this:

<button onClick={() => {dispatch(deletePost(postId))}}>Delete</button>

Your final Post.js Component should look like this:

import React from 'react'import {useDispatch} from 'react-redux'import {deletePost} from '../../../_actions/posts'export default function Post({setId, author, title, article, createAt, postId}) {const dispatch = useDispatch()return (<><div><p>Author: {author}</p><h1>{title}</h1><h3>{article}</h3><span>{createAt}</span><div><button onClick={() => {setId(postId)}}>Update</button><button onClick={() => {dispatch(deletePost(postId))}}>Delete</button></div></div></>)}

STEP 14: Testing our app

Congratulation guys, you have just learned how to build a full stack application using the MERN stack.

LINK TO THE REPO

If you found this tutorial useful, you can give it a clap, or if you have any questions, you can find me on Linkedin or Twitter.

--

--

Eli Sultanov
Geek Culture

Sharing with the world things I discover while coding. Find me on Twitter @elidotsv or at https://elisv.com