Creating the user interface for our application
When designing the structure of a frontend, we should also consider the folder structure, so that our app can grow easily in the future. Similar to how we did for the backend, we will also put all our source code into a src/
folder. We can then group the files in separate folders for the different features. Another popular way to structure frontend projects is to group code by routes. Of course, it is also possible to mix them, for example, in Next.js projects we can group our components by features and then create another folder and file structure for the routes, where the components are used. For full-stack projects, it additionally makes sense to first separate our code by creating separate folders for the API integration and UI components.
Now, let’s define the folder structure for our project:
- Create a new
src/api/
folder. - Create a new
src/components/
folder.
Tip
It is a good idea to start with a simple structure at first, and only nest more deeply when you actually need it. Do not spend too much time thinking about the file structure when starting a project, because usually, you do not know upfront how files should be grouped, and it may change later anyway.
After defining the high-level folder structure for our projects, let’s now take some time to consider the component structure.
Component structure
Based on what we defined in the backend, our blog application is going to have the following features:
- Viewing a single post
- Creating a new post
- Listing posts
- Filtering posts
- Sorting posts
The idea of components in React is to have each component deal with a single task or UI element. We should try to make components as fine-grained as possible, in order to be able to reuse code. If we find ourselves copying and pasting code from one component to another, it might be a good idea to create a new component and reuse it in multiple other components.
Usually, when developing a frontend, we start with a UI mock-up. For our blog application, a mock-up could look as follows:
Figure 4.1 – An initial mock-up of our blog application
Note
In this book, we will not cover UI or CSS frameworks. As such, the components are designed and developed without styling. Instead, the book focuses on the full-stack aspect of the integration of backends with frontends. Feel free to use a UI framework (such as MUI), or a CSS framework (such as Tailwind) to style the blog application on your own.
When splitting up the UI into components, we use the single-responsibility principle, which states that every module should have responsibility over a single encapsulated part of the functionality.
In our mock-up, we can draw boxes around each component and subcomponent, and give them names. Keep in mind that each component should have exactly one responsibility. We start with the fundamental components that make up the app:
Figure 4.2 – Defining the fundamental components in our mock-up
We defined a CreatePost
component, with a form to create a new post, a PostFilter
component to filter the list of posts, a PostSorting
component to sort posts, and a Post
component to display a single post.
Now that we have defined our fundamental components, we are going to look at which components logically belong together, thereby forming a group: we can group the Post
components together in PostList
, then make an App
component to group everything together and define the structure of our app.
Now that we are done with structuring our React components, we can move on to implementing the static React components.
Implementing static React components
Before integrating with the backend, we are going to model the basic features of our application as static React components. Dealing with the static view structure of our application first makes sense, as we can play around and re-structure the application UI if needed, before adding integration to the components, which would make it harder and more tedious to move them around. It is also easier to deal only with the UI first, which helps us to get started quickly with projects and features. Then, we can move on to implementing integrations and handling state.
Let’s get started implementing the static components now.
The Post component
We have already thought about which elements a post has during the creation of the mock-up and the design of the backend. A post should have a title
, contents
, and an author
.
Let’s implement the Post
component now:
- First, create a new
src/components/Post.jsx
file. - In that file, import
PropTypes
:import PropTypes from 'prop-types'
- Define a function component, accepting
title
,contents
, andauthor
props:export function Post({ title, contents, author }) {
- Next, render all props in a way that resembles the mock-up:
return ( <article> <h3>{title}</h3> <div>{contents}</div> {author && ( <em> <br /> Written by <strong>{author}</strong> </em> )} </article> ) }
Tip
Please note that you should always prefer spacing via CSS, rather than using the <br />
HTML tag. However, we are focusing on the UI structure and integration with the backend in this book, so we simply use HTML whenever possible.
- Now, define
propTypes
, making sure onlytitle
is required:Post.propTypes = { title: PropTypes.string.isRequired, contents: PropTypes.string, author: PropTypes.string, }
Info
PropTypes
are used to validate the props passed to React components and to ensure that we are passing the correct props when using JavaScript. When using a type-safe language, such as TypeScript, we can instead do this by directly typing the props passed to the component.
- Let’s test out our component by replacing the
src/App.jsx
file with the following contents:import { Post } from './components/Post.jsx' export function App() { return ( <Post title='Full-Stack React Projects' contents="Let's become full-stack developers!" author='Daniel Bugl' /> ) }
- Edit
src/main.jsx
and update the import of theApp
component, because we are now not usingexport
default
anymore:import { App } from './App.jsx'
Info
I personally tend to prefer not using default exports, as they make it harder to re-group and re-export components and functions from other files. Also, they allow us to change the names of the components, which could be confusing. For example, if we change the name of a component, the name when importing it is not changed automatically.
- Also, remove the following line from
src/main.jsx
:import './index.css'
- Finally, we can delete the
index.css
andApp.css
files, as they are not needed anymore.
Now that our static Post
component has been implemented, we can move on to the CreatePost
component.
The CreatePost component
We’ll now implement a form to allow for the creation of new posts. Here, we provide fields for author
and title
and a <textarea>
element for the contents of the blog post.
Let’s implement the CreatePost
component now:
- Create a new
src/components/CreatePost.jsx
file. - Define the following component, which contains a form to enter the title, author, and contents of a blog post:
export function CreatePost() { return ( <form onSubmit={(e) => e.preventDefault()}> <div> <label htmlFor='create-title'>Title: </label> <input type='text' name='create-title' id='create-title' /> </div> <br /> <div> <label htmlFor='create-author'>Author: </label> <input type='text' name='create-author' id='create-author' /> </div> <br /> <textarea /> <br /> <br /> <input type='submit' value='Create' /> </form> ) }
In the preceding code block, we defined an
onSubmit
handler and callede.preventDefault()
on the event object to avoid a page refresh when the form is submitted. - Let’s test the component out by replacing the
src/App.jsx
file with the following contents:import { CreatePost } from './components/CreatePost.jsx' export function App() { return <CreatePost /> }
As you can see, the CreatePost
component renders fine. We can now move on to the PostFilter
and PostSorting
components.
Tip
If you want to test out multiple components at once and keep the tests around for later, or build a style guide for your own component library, you should look into Storybook (https://storybook.js.org), which is a useful tool to build, test, and document UI components in isolation.
The PostFilter and PostSorting components
Similar to the CreatePost
component, we will be creating two components that provide input fields to filter and sort posts. Let’s start with PostFilter
:
- Create a new
src/components/PostFilter.jsx
file. - In this file, we import
PropTypes
:import PropTypes from 'prop-types'
- Now, we define the
PostFilter
component and make use of thefield
prop:export function PostFilter({ field }) { return ( <div> <label htmlFor={`filter-${field}`}>{field}: </label> <input type='text' name={`filter-${field}`} id={`filter-${field}`} /> </div> ) } PostFilter.propTypes = { field: PropTypes.string.isRequired, }
Next, we are going to define the
PostSorting
component. - Create a new
src/components/PostSorting.jsx
file. - In this file, we create a
select
input to select which field to sort by. We also create anotherselect
input to select the sort order:import PropTypes from 'prop-types' export function PostSorting({ fields = [] }) { return ( <div> <label htmlFor='sortBy'>Sort By: </label> <select name='sortBy' id='sortBy'> {fields.map((field) => ( <option key={field} value={field}> {field} </option> ))} </select> {' / '} <label htmlFor='sortOrder'>Sort Order: </label> <select name='sortOrder' id='sortOrder'> <option value={'ascending'}>ascending</option> <option value={'descending'}>descending</option> </select> </div> ) } PostSorting.propTypes = { fields: PropTypes.arrayOf(PropTypes.string).isRequired, }
Now we have successfully defined UI components to filter and sort posts. In the next step, we are going to create a PostList
component to combine the filter and sorting with a list of posts.
The PostList component
After implementing the other post-related components, we can now implement the most important part of our blog app, that is, the feed of blog posts. For now, the feed is simply going to show a list of blog posts.
Let’s start implementing the PostList
component now:
- Create a new
src/components/PostList.jsx
file. - First, we import
Fragment
,PropTypes
, and thePost
component:import { Fragment } from 'react' import PropTypes from 'prop-types' import { Post } from './Post.jsx'
- Then, we define the
PostList
function component, accepting aposts
array as a prop. Ifposts
is not defined, we set it to an empty array, by default:export function PostList({ posts = [] }) {
- Next, we render all posts by using the
.map
function and the spread syntax:return ( <div> {posts.map((post) => ( <Post {...post} key={post._id} /> ))} </div> ) }
We return the
<Post>
component for each post, and pass all the keys from thepost
object to the component as props. We do this by using the spread syntax, which has the same effect as listing all the keys from the object manually as props, like so:<Post title={post.title} author={post.author} contents={post.contents} />
Note
If we are rendering a list of elements, we have to give each element a unique key
prop. React uses this key
prop to efficiently compute the difference between two lists when the data has changed.
We used the map
function, which applies a function to all the elements of an array. This is similar to using a for
loop and storing all the results, but it is more concise, declarative, and easier to read! Alternatively, we could do the following instead of using the map
function:
let renderedPosts = [] let index = 0 for (let post of posts) { renderedPosts.push(<Post {...post} key={post._id} />) index++ } return ( <div> {renderedPosts} </div> )
However, using this style is not recommended with React.
- We also still need to define the prop types. Here, we can make use of the prop types from the
Post
component, by wrapping it inside thePropTypes.shape()
function, which defines an object prop type:PostList.propTypes = { posts: PropTypes.arrayOf(PropTypes.shape(Post.propTypes)).isRequired, }
- In the mock-up, we have a horizontal line after each blog post. We can implement this without an additional
<div>
container element, by usingFragment
, as follows:{posts.map((post) => ( <Fragment key={post._id}> <Post {...post} /> <hr /> </Fragment> ))}
Note
The key
prop always has to be added to the uppermost parent element that is rendered within the map
function. In this case, we had to move the key
prop from the Post
component to Fragment
.
- Again, we test our component by editing the
src/App.jsx
file:import { PostList } from './components/PostList.jsx' const posts = [ { title: 'Full-Stack React Projects', contents: "Let's become full-stack developers!", author: 'Daniel Bugl', }, { title: 'Hello React!' }, ] export function App() { return <PostList posts={posts} /> }
Now we can see that our app lists all the posts that we defined in the
posts
array.
As you can see, listing multiple posts via the PostList
component works fine. We can now move on to putting the app together.
Putting the app together
After implementing all the components, we now have to put everything together in the App
component. Then, we will have successfully reproduced the mock-up!
Let’s start modifying the App
component and putting our blog app together:
- Open
src/App.jsx
and add imports for theCreatePost
,PostFilter
, andPostSorting
components:import { PostList } from './components/PostList.jsx' import { CreatePost } from './components/CreatePost.jsx' import { PostFilter } from './components/PostFilter.jsx' import { PostSorting } from './components/PostSorting.jsx'
- Adjust the
App
component to contain all the components:export function App() { return ( <div style={{ padding: 8 }}> <CreatePost /> <br /> <hr /> Filter by: <PostFilter field='author' /> <br /> <PostSorting fields={['createdAt', 'updatedAt']} /> <hr /> <PostList posts={posts} /> </div> ) }
- After saving the file, the browser should automatically refresh, and we can now see the full UI:
Figure 4.3 – Full implementation of our static blog app, according to the mock-up
As we can see, all of the static components that we defined earlier are rendered together in one App
component. Our app now looks just like a mock-up. Next, we can move on to integrating our components with the backend service.