Build a Shopping Cart using React and TypeScript
By the end of this tutorial, you will have the key-concepts to build a Shopping Cart App using React.js with TypeScript. The project will be similar to the image below. You can check it out “alive” here.
Index
- Requirements for this tutorial
- Useful resources
- TypeScript fundamentals
- Using TypeScript with React.js
- Component architecture
- Calling fakestore API
- Printing data on the UI
- State management using React hooks
- Updating the state
- Dynamic routing
- Adding products to the shopping cart
- Summing up
- Challenges for you
Requirements
In order for you to enjoy this tutorial, you must have previous knowledge in
- Html
- Css
- JavaScript (ES6+)
- React.js
- TypeScript (optional)
Useful resources
I would like to share with you the Figma canvas I based the design of this project. You can copy it to your own drafts and customize it yourself.
As well, I want to show you the final repository for you to review it in case of doubt or to use it as a benchmark.
Finally, you can reach me out on twitter whenever you want…
TypeScript fundamentals
In order for you to practice TS, I highly recommend to use codesandbox.io. You can start a Vanilla TypeScript template in order to start learning.
TypeScript is a superset of JavaScript, therefore, if you already know JavaScript, we can say you know TypeScript as well. However, TS offers us a better development experience helping us detecting errors beforehand thanks to the typed variables feature. Here is an example of TS syntaxis.
As we can see above, we got an error everytime we use an incorrect data type in a certain variable. This is the magic of TS, we can avoid “unseen” mistakes beforehand.
To assign a data type to a variable, we use the following sintaxis.
const variableName: dataType = variableValue
.
Now, what if a variable could be string or a number? In that case, we do as shown below
As you can notice, we used the Union Operator|
to tell TS about the double data type variable.
Now that you have an introduction to TS, let’s take a look at all existing data types:
String, number and boolean
These are the simplest data types in TS. I suppose you are already familiar with them, therefore, let’s jump into the next ones.
Any, null, undefined, unknown
From here on we are going to see “new” data types. Lets’s start with the worst of all of them
- Any: Considered as the worst data type in TS because of its ability to cancel or “forget” the typed feature. In other words, every time we use any as a data type, TS ignores that variable. Here is an example:
As we can see above, the variable foo
has a data type of any and it has been initialized as null. Because of the data type, we can add properties to foo
as in the case of an object literal. Even though we know this is NOT correct, TS apparently is OK with this until we try our program on the browser.
- Null and undefined: This two data types are common in web development and in JavaScript are used too.
null
makes reference to an empty value andundefined
to a undefined value… they are very self-describing, aren’t they?
- Unknown: It was introduced in TS v3 and it tries to solve some problems of
any
. First of all, we can use this data type when we don’t know what will be the value of a given variable.
As you can notice above, variable bar
started as unknown and after some processes in the program, its value changed. However, TS was OK with all the changes made to the variable.
Unlike any, with unknown we cannot make weird stuff.
Functions in TypeScript
In case of functions, the parameters can be typed (otherwise, their default data type is any
) and we can type the returned value of the functions as well.
As we can see above, the last example is the correct one, because we are adding the return value data type. The first example is wrong, because we are not seizing TS potential and the second is relatively correct.
But what if my function doesn’t return anything, what can I do in that scenario?
For those cases, we use void
, just as shown below
In the image above we can see another scenario: never. This data type means that a certain function will never finish to execute completely. This case is not used commonly, but it is important for you to know it.
Objects & arrays in TypeScript
In case of arrays, there are 2 sintaxis options:
In relation of objects, there is an object
data type, but it is not recommended to use, because it is very general.
The object person
above works fine, but we don’t know if there is a missing property or if there are more properties than necesary. And even worse, we don’t know if the properties have their correct data types. That’s why interfaces are used for.
Interfaces and custom types
Interfaces are made to provide a more accurate data type for objects.
As you can see above, now we have an exact data type for the given objectperson
. Moreover, we can specify properties that are functions and properties that are optional for the object.
In the case of country, we are specifying it is an optional property with the mark ?
. In getName(), we are declaring it as a function that returns a value of type string.
However, we can enhance this even further. How? Adding custom types. These are types that we can create. Let’s say, for example, that the property “name” of the previous example can be (for some strange reason) boolean, string, number or even null. In that case, we can do something like this
As you can notice, this is not scalable. Then, here is when custom types come into the game.
With custom types, we have a more scalable code. And with this said, you are ready to start using TypeScript in your React apps.
There is another important feature in TypeScript; enums. But it is your homework to search about them.
Using TypeScript with React.js
For this tutorial, we are going to use create-react-app to start coding as fast as possible, althought in my opinion, I prefer to config webpack and my module bundler on my own, but we can spend a whole tutorial just doing that.
According to create-react-app docs, we must type the code below to start a project using TypeScript.
npx create-react-app my-app --template typescript
Once it has finished to load, we enter to our project directory and run the following command: npm run start
. If a new tab has opened in your browser and you can see React logo rotating in the middle of the screen or something similar to the image below, then we can continue.
Component architecture
The way how we organize our files and directories is almost as important as the code itself. For this tutorial, I am going to use components, containers and pages in my organization.
Each of this is going to have their own directory in the source code.
It’s up to you if you want to use another organization for your files, but in case you want to try with the one proposed in this tutorial, I would like to explain it even further.
- Components: These are the most basic component in our app, a button is a good example of this.
- Containers: These are the combination of two or more individual components. In case of a search field, a button and the input can form a container. However, we can have containers made of two or more containers, for example, the header is a container made of the containers SearchField and Navbar.
- Pages: These are the pages in our app. For example, we can have Home or About page, etc. Pages are made of many containers and individual components.
Calling fakestore API
Without further ado, let’s get our hands in the keyboard. In order to start, it’s important to notice all the data printed in the UI is going to be pulled from fakestore API. So, first of all, we are going to fetch the data and show it up on the console, making sure we are receiving the data correctly.
In order to achieve that result, I like to use useEffect() hook to pull the data everytime the component finishes to render.
If successful, we should see an array of objects printed in the console. Each object is resembling a product in the shopping cart.
Printing data in the UI
Now that we are already receiving data from the API, we can start printing that data in the UI. But first of all, we need to know the structure of the data. The following endpoint https://fakestoreapi.com/products
returns an array of 20 objects. Each object represents a product for the shopping cart. And below we have the first product data.
With this in mind, we can create a component to show all the information of a single product.
Component structure
For this tutorial, every component built, either a Page, Container or individual Component is going to have its own directory instead of just a single file. In my opinion, I think this is better because we can have the styles file, utils and others relevant files in the component’s directory.
As you can see in the example above, we have two containers and both are directories instead of single files. Therefore, instead of have a file Product.tsx for our component, we have an index.tsx file inside Product directory.
As always, it’s up to you if you want to structure your components this way.
Coding the Product component
First of all, we are going to create a new directory in src/containers/
. This directory is going to be called Product. Inside it, we create index.tsx file and this file will contain the actual component.
In the image above, you can see the component Product, and if you looked closely, most probably you noticed three important things that I am going to explain you:
- React.FC stands for Functional Component. This is how we indicate TS that this is a React component.
- JSX.Element refers to the data type this function is supposed to return.
- I am not exporting the component as default, because when we do so, we can change the name of the component. It was made this way to keep consistency in the code.
The data used in this component is hard-coded. This is a good practice to handle styles of the components before to start using real data.
Printing the Product on the UI
We already have our Product component. That’s ok, but if we run our app, nothing has changed, right? This is due to the fact that we have not imported it to App.tsx file. Let’s do so.
We remove all the JSX inside the <div>
App and add the <Product />
component instead. That’s it.
If you see the app on the browser, you will see a huge image of the product, so… it’s working! but we need to add some styles.
Adding styles
As mentioned before, for this tutorial, each component is a directory instead of just a single file. Thanks to this structure, we can have the styles file in the src/containers/Product
directory.
For simplicity’s sake, I am going to use plain css for this tutorial, but again, feel free to use whatever you want to style your components.
This is the css file. However, if we take a look at our app, it is still the same… that’s because we need to import the styles from src/containers/Product/index.tsx
.
What I want you to notice here is the second line, where we are importing the styles. Now, let’s see our app on the browser.
Remember to use
npm run start
to start your app on local.
If everything is OK, we should see something similar to this on the browser.
So far we have printed one hard-coded Product on the screen. That’s great! Nevertheless, the idea is to show all of them in a non-hard-coded way, that’s what we are going to do next.
State management using React hooks
For this tutorial we are going to use useReducer() and useContext() to handle state, avoiding prop-drilling and making our app scalable.
Workflow
Let’s start reviewing some concepts before implementing all this. First of all, we are going to have a global state as only source of truth.
With useReducer, we are going to be able to change the state in our app in a function apart from the UI code, making our components simpler and cleaner. Not to mention the fact that we are going to have another function (dispatch), which let us perfom changes in our state, making a really declarative code.
Finally, with useContext we are going to avoid prop-drilling, making our app scalable.
Let’s get our hands dirty.
useContext()
This hook is used to create a global context of the state. We are going to create a new file index.ts in the path src/context
.
As we can see, we initialize our context as null, but its value will change to the global state in the future, that’s why we add <StateInterface | null>
to the createContext
function.
Before moving on, we are missing something: the globalTypes.ts file. This file is going to contain global interfaces and custom types that we might use in other files in our app.
ProductInterface
is the “template” for each of our products. So far we are going to have the properties shown above, althought we must have an ID too.
Now that we have created our context, we still need to wrap our whole app in a context Provider. Without this provider, our components will not have access to the global state.
We are importing ctx from ./context
, therefore, we can access its Provider property, which has the compulsory property value, where we are adding our state. With this we are done and our app should work fine…
However, we have a problem, because we can execute even better than this. In order to make our app scalable, we can use useReducer hook.
Don’t worry if you have never used useReducer before, I will explain everything you need to know about it.
useReducer()
This hook is very simple. From it, we receive the state and a special function able to modify the state (dispatch), but it consumes a function with all possible changes in the state (reducerFn) and the initial state. Here is its sintaxis.
import { useReducer } from "react";const [state, dispatch] = useReducer(reducerFn, initialState)
First of all, we need to create an index.ts file in the new folder src/reducer
. In this file we are going to write 2 things:
- reducerFn: Function with all possible changes made to the state.
- initialState: Our app’s initial state…
The reducerFn has 2 parameters:
- state: This is the global state
- action: This action will contain the “instructions” to make changes in the state. This actions has its own interface ActionInterface. Below you can see this interface more in detail.
// src/globalTypes.ts// @other interfaces here...export interface ActionInterface{
type: string;
payload: unknown;
}
reducerFn only returns the state so far, we are going to update this file along with this tutorial.
Updating the state
Until now our app is wrapped in <ctx.Provider>
and we already have the initialState and reducerFn in src/reducer/index.ts
. This is the initial setup, now, let’s implement it.
In the code above there are 2 things I want you to notice:
- The useReducer is being applied with the imports from
./reducer
. - In useEffect, intead to console the data, we are using the dispatch function already.
Dispatch function receives an object as parameter, in this object we specify the type (the title of the change we are going to perfom) and the payload (information we can send to modify the state).
So, in this case we want to perfom the action “ADD_PRODUCTS” and we are sending the data
object to use it in order to change the state.
If we run our app, most probably we are going to get an error, because there is still another step we need to do. We need to edit our reducerFn
to handle the ADD_PRODUCTS case and change our global state somehow.
We are using a switch to handle any case we may encounter. Now, in order to make sure our app is actually working, let’s make a console.log of our state.
Apparentely, nothig has changed, but if we check our console, we should see two objects, both representing the state. The first one is the state at the beginning of our app and the second after the change made in useEffect.
If that’s your case, then we are ready to move on.
Printing real data in the UI
Now that the state changes within useEffect, we can print the 20 products pulled from fakestore API pretty easily.
A simple validation is happening inside <div className="App">
. We can read this validation as follows: “If state.products is not empty, print the hard-coded component. Otherwise, print Loading… message”.
Now, in the UI we can see a Loading… message for some seconds before to see the hard-coded component. Now, let’s do that with all 20 products we have available.
We are mapping state.products array and hence, we are watching 20 hard-coded component, but unlike the previous examples, we are doing two important things here:
- key property: Every time we map an array with React to print components in the UI, each of this must have a key property with a unique value.
- Fragment: Fragment is used to wrap the mapping becasue React just receives one child per render. In this case, we are sending 20 children per render (the 20 components returned), that’s why Fragment is necessary.
Fragment is imported from “react”
So far we have printed on the screen 20 hard-coded components. But now is time to customize each component.
Customizing components with props
Firs of all, we need to edit our <Product />
component for this being able to receive and display props.
We are adding only two parameters to the Product component, the title and the image url. However, here it is important to notice the data type of our component. This is React.FC<ProductProps>
. When we use React.js with TS, this is compulsory, because if we use only React.FC
, apparentely the component doesn’t receive any parameters.
We need to make other configs before getting ready.
In this step, we need to change the ProductInterface made in the file src/globalTypes.ts
. This interface should look like this
export interface ProductInterface{
title: string;
image: string;
}
Now run your app on local. If everything is OK, you should see 20 products on the screen. In my case I added some styles using grid and now this is what I see on the UI.
You can add whatever styles you want.
Dynamic routing
So far we have achieved a lot. What we are going to do next is the dynamic routing. But wait… what is that? It is the method to display dynamic information in a certain page.
In order to start doing so, we need to setup our router. We do this as follows.
Setting our router
First of all, we need to install react-router-dom. We do it with the following command.
npm install react-router-dom
Then, we are going to create the index.tsx file in src/pages/Home
.
This is our Home page, but if you look closely, you will notice it is very similar to what we return in the App.tsx file. That’s why the App.tsx file will change drastically.
As you can notice, now our code is way cleaner, althought I have to confess something. The <main>
tag wrapping Home was added at the moment I made some styles to show the grid for the products, just for you to keep it in mind.
Try your app on the browser, it should work as fine as usual.
Moving on, let’s create the router in the App.tsx file.
Now we have added the BrowserRouter component imported from react-router-dom
, this component is in charge of routing. The <Home />
page is in the root of the app (“/”) and now we need to create the page for the dynamic routing.
ProductDetail page
To do it, we need to create another page, which name will be ProductDetail. So, we create the index.tsx file in src/pages/ProductDetail
.
We start creating this page as simple as possible, just to make sure it is working as expected but now we need to edit the router, which is in App.tsx file.
We have just added another route, but this is dynamic, that why we are using the placeholder :title
as the identifier.
Let’s try our app. Paste the following url in your browser and see what happens.
http://localhost:3000/products/aproduct
If you get the “Product Detail Page here!” message, we are OK. Otherwise, debug your app to see what’s going on. But now, let’s display dynamic information. In order to do it, we first need to config Product component in src/containers/Product/index.tsx
to trigger an action onClick().
Here I want you to notice 4 important things:
- I am using useNavigate hook to move to another pages. This hook is imported from
react-router-dom
. - I am not using this hook directly. I have to initialize a variable called navigate and then use it.
- onClick() is being used in the
<div className="Product">
tag. This is not the best practice, but for the example’s sake, it works fine. - trim() function is used to avoid spaces at the beginning and in the end of the string.
Now, everytime we click <Product />
, we are going to navigate to the dynamic page, but this page is still static, let’s change that asap.
We are using useParams hook from react-router-dom. From this hook we get the placeholders in the routes, thanks to that, we can access the title of each product and now, everytime we enter in a dynamic route, we are going to see the title of the product displayed on the screen.
This is great! But it’s useless… We are accessing to the placeholder of the route only, the idea is to get all the information of the product, right? How can we do that?
We do that as follows.
Such a thing, what the hell is just happening? Let me explain you all the process…
First of all, I am able to access to the global state with the useContext hook, therefore, I have acess to state.products
.
With access to this data, we use find method to get information of the product. Finally, we show that info on the console. Everytime you access to the dynamic route, on the console you will see the whole object.
Now, it is your work to use that data and print it on the UI.
How to add products to the shopping cart?
This is the last feature we are going to develop in this tutorial. We are going to add an “Add to cart” button to the dynamic page and a header where we will have access to the shopping cart. Our goal is to make the button work properly. Let’s get started.
Adding a Layout to our project
A Layout is part of the UI that will appear in EVERY page in the app. In our case, we need to create a Navbar or Header to display in every page. To do so, we first need to create index.tsx file in src/layout
.
Now I want to share with you the css I am using for the <section className="products-container">
.
Now, if we only do this, nothing in our app will change, we need to apply it out.
The <Layout />
is imported from ./layout/index.tsx
and it wraps our Routes, therefore, we are going to have the Layout elements in all our pages.
Now, it is time to improve our layout, adding some actions.
In this improved version of the <Layout />
, we just added the navigate feature. However, here we are doing a mistake, we are calling to a navigate(“/cart”) route, but this route doesn’t exist yet. We need to create it.
I hope you can be familiar with the process and that’s why your challenge will be the creation of this new route (cart). The steps are:
- Create a new page called “Cart” in
src/pages/Cart/index.tsx
, with a simple JSX. - Add it to the router in
App.tsx
.
Once you have finished the challenge, we need to change 2 things in our global settings: initialState, StateInterface. The changes will be made to add another property: shoppingCart.
Now, we only need to add a Call To Action in the dynamic page to add the <Product />
to the cart. In my case, I will add a button.
As you can see, my ProductDetail page right now is very simple. The idea here is you taking advantage of all the information we get from product
variable.
Either way, the button added just send a message to the console, let’s make something more valuable.
Now the button is calling the dispatch function. Function that actually is a parameter of the component. Most probably, you are getting an error, that’s because we need to make two more configurations to get this done.
The first configuration was in the router. Now <ProductDetail />
page is receiving the dispatch function as prop. With this, you should not have any error. However, if you click the “Add to cart” button in <ProductDetail />
, this will not work. Let’s fix that.
The second configuration was in src/reducer/index.tsx
. As you can see here, we are modifying the reducerFn and adding a new case. In this new case, we are just adding the new product to the shoppingCart array and that’s it.
With the last 2 configurations made, our program is supposed to work properly, but in order to see it on the UI, let’s do one last configuration.
We have just modified <Cart />
page and there is a simple validation that we can read as follows: “If state.shoppingCart is empty, display empty message, otherwise, map state.shoppingCart”.
If you followed all these steps successfully, everytime you add a Product to the cart, this product should be seen in the <Cart />
page.
Summing up
Congratulations to get this far. In this tutorial you learned:
- TypeScript fundamentals
- How to build pages using TypeScript + React.js
- Use several React.js hooks, such as useReducer and useContext
- Modify the state in a scalable way
- Use dynamic routing
However, there is a lot to do yet….
Challenges for you
I encourage you to make this app as large as you want, please add the following features:
- Once we have a Product in the cart, the button should change to “Remove”, and for sure, it should work
- Adding nice styles
- Create a 404 Error page
These are the only features I have in mind right but I know the limit is your imagination.
Finally, if you liked this tutorial, please follow me on twitter and let me know you liked it or give me some feedback too, that’s motivating for me. Thanks!