Built with Vite + React & Hosted by Netlify
You can see all tickets created & closed here : Closed Tickets β
Welcome to my Ecommerce Project Repository, a portfolio project of mine!
This project serves as a personal learning experience, allowing me to test my abilities in seeing a project through from start to finish. Rather than replicating existing stores, the focus is on developing robust business logic and integrating various technologies.
My goal was to fully immerse myself in building an ecommerce application, exploring different topics and honing my implementation and integration skills. While efficiency could have been prioritized, the main objective was to gain comprehensive knowledge.
You can see all tickets created & closed here : Closed Tickets β
Click here to see all the goals:
- Build the project initially using Vanilla JavaScript and later convert it into a TypeScript project.
- Initially implement styling with SCSS and then convert it into Styled Components styling
- Utilize Firebase for authentication and Firestore as the database
- Begin with Context hooks and reducers, then transition to legacy Redux,
- Followed by integrating Redux Thunk and then convert it into Redux Saga,
- Finally convert the legacy Redux into modern Redux using Redux-Toolkit
- Integrate GraphQL and Apollo into the project
- Implement performance optimizations to ensure fast and smooth user experience
- Enhance security by implementing Firebase Rule Security to protect user data
- Write comprehensive tests for React components and functionality
- Convert the project into a Progressive Web App (PWA) to provide a seamless and responsive experience across different devices
In this section, I'll guide you through the step-by-step process of bringing this project to life. From initial concept to final implementation, I've documented my progress, highlighting the tools, technologies, and methodologies used along the way.
Please note that this project was primarily a learning experience. While there may have been more efficient approaches, I intentionally explored specific topics and tested my abilities with various technologies and libraries.
Join me on this behind-the-scenes adventure as I share exciting milestones, challenges, and valuable lessons learned as a solo developer. Experience the transformation of an idea into a fully functional ecommerce platform.
Click here to expand and see all the steps I took to build this project:
Click here to see more:
Click here to see more:
-
Using Vite (pronounced 'Veet'π€)
npm create vite@latest
-
Clearing up some of the unnecessary and boiler plate code
-
Adding and setting up SASS - Syntactically Awesome Style Sheets
npm install sass
-
Setting up the most essential folder structure for components
Click here to see more:
-
- `npm install react-router-dom localforage match-sorter sort-by` - fixing any conflicts from `npm` audit
-
- Importing { Routes, Route } from 'react-router-dom' `
- Wrapping everything in Routes
- Creating a Route path to the Home page
-
- Importing
{ BrowserRouter } from 'react-router-dom'
- wrapping the "App" with
BrowserRouter
- Importing
Click here to see more:
-
registered the Ecommerce-projec/app in the FireBase web-console
-
- npm install firebase - Added a `Utils` Folder with `FireBase folder` - Added file called `firebase.utils`
-
- import { initializeApp } from 'firebase/app'; - Setting up authentication Import {} from `firebase/auth`; - getAuth - signInWithRedirect, - signInWithPopup, - GoogleAuthProvider, - Created firebaseConfig with info from FireBase web-app - Initialized the App with firebaseConfig - Enforcing account selection - In the Firebase console - enabled the Google Sign in method
-
- import {signInWithGooglePopup, createUserProfileDocument} from 'firebase.utils'; - Creating Async Sign-in Function/Method to get access token
Click here to see more:
Cloud Firestore Doc's Obviously we are just using FireBase for authentication. To be able to store the users, I will need a database. Enter Cloud FireStore.
-
via web-console and based in EU amended the rules
-
- { getFirestore, doc, getDoc, setDoc }
-
- used an if statement with try block, so as if user does not exist do 'x' or else return user.
- used a catch block , so as if an error occurs console.log the error.
- logging date data for creation of new users (new Data()).
-
npm install vite-plugin-svgr
- Updated the vite.config.js
Click here to see more:
-
- Using
useEffect
Hook fromreact
- Using
getRedirectResult
fromfirebase/auth
- Amended the SignIn method to incorporate the above
- Using
The users profile will be present in many components throughout the app (Such as a Signing-in, profile management, shopping cart etc.) Will require having to have access to the 'user' context through out the app
-
-
Created a user.context.jsx file, using;
- useState
- createContext
-
Created UserContext to store the users data (& null as defaults), as well as export into components.
-
Create
userProvider
to pass this into/wrap the entire app. -
Obviously wrapped the app inside in Main.jsx
-
-
- When 'user' signs in, store the 'user' into the context
- Added
useContext
& imported thecontext
object - Added the
SetCurrentUser
functionality to the Sign-in method - Running
SetCurrent
user if sign-in promise returns a 'user'. - (Both For google and email sign in)
-
- Added
UseContext
& imported thecontext
object - Added the currentUser = useContext(UserContext)
- Added
-
- When 'user' creates an account, store 'user' into the context
- Added
useContext
& imported thecontext
object - Added the
SetCurrentUser
functionality to the Sign-in method - Running
SetCurrent
user if sign-in promise returns a 'user'
-
- If user signed in, 'sign-in' link should turn into 'sign-out'
- Simple Conditional (ternary) operator in the jsx
Obviously once the user is signed in, they will need to be able to sign-out.
-
- imported
signOut
from firebase/auth - Created an export for
signOutUser
- imported
-
- Imported the
SignOut
Function fromFireBase Utils
- Updated the conditional sign-out link with an onClick with
signOutUser
- Imported the
-
- await
signOutUser
- then
setCurrentUser
= null; - updated the sign-out link to use the
signOutHandler
- await
Click here to see more:
Just as I finished this I learned read about Observer's and onAuthStateChange
This is actually essential otherwise the user will have this unwanted persistent-signed-in-state - i.e. the user will remain signed-in, even if we refresh the page.
The problem being it will appear if we have to sign-in again but in the console if we log our user, it will show the last signed in user.
Additionally we dont need to hook into the sign and sign out components any longer and therefore don't have to re-run all those fucntions on a change - instead our onAuthStateChange listener will do this for us.
Ultimatly saving some computing power. Wish I read about this before coding all that. lol
-
- imported this on AuthChange
- created an export for it
-
- added the
useEffect
Hook - added new functions from
firebase.utils
- created a hook that will track the authChange;
- Needed to prevent a memory leak
- Needed to use effect cleanup to dispose when no longer needed.
- added the
-
no longer require to run the functions everytime a change in sign-in occurs , therefore;
-
removed all
context
imports -
removed all
setCurrentUser
functions -
Google sign in function;
- removed
createUserDocumentFromAuth
- added this function to the user.context (the above can't be done with sign-up)
- removed
-
-
- no longer required to
setCurrentUser
- removed
signOutHandler
function
- no longer required to
Click here to see more:
-
Created a simple JSON file with some simple clothing store items (just hats). I will use this to mock having content for the store temporarily while I finish building some of the essential functionality, then later I will remove/replace the hard coded shop data.
-
- Imported React-Context
- Imported the ProductsContext and destructuring to get the 'products' source
- Mapping over the Products to display individual items on the site
-
- Imported
create context
andshop-data
- Created ProductsContext export
- Created ProducsProvider export to wrap the app inside
- Imported
- I want the products be able to access the user context
(this could be a debate but for my app, I think this would be the simplest)
- Wrapped the <App> inside <ProductsProvider>
- Removed the old temporary 'blank' shop page
- Imported the newly created shop component
-
- Imported Button component
- Created basic scss styling sheet and imported into the component
- Getting individual clothing items from passing in the products and destructuring (This will rely on receiving the individual items in the Shop.route file - via mapping)
- Created layout of the card and returning the following in the card;
- Image
- Name
- Price
- Button
Click here to see more:
-
- Added SVG image to the assets folder
-
- Imported styles sheet, SVG file,
- Created a function to set the cartContext from false to true
- Function is run with an onClick (Cart-Icon Button)
-
- updated the overall styling to make things easier to look at.
- added the cart-component
- added the drop-down component
-
- simple layout with button for a checkout (future)
- created simple styling
-
-
Very simple setup - is the cart open set to false by default
-
Set the cart open
-
Imported CardProvider to App.js
-
Imported isCartOpen to Navigation using a short-circuit operator (&&)
-
Click here to see more:
-
- Created simple cart item that will return, using
destructuring
;- the
Image
of the item - the
Name
of item - the
Price
of the item (Quantity x Price
)
- the
- Created simple cart item that will return, using
-
-
In the
CartContext
;- Added
CartItems Array
- Added
addItemToCart
function
- Added
-
In the
CartProvider
;- Added useState
[cartItems, setCartItems] = useState([]);
- Added
addItemToCart
function - Updated the value to include
cartItems
&addItemToCart
- Added useState
-
Created new export
addCartItem
helper funciton;- This will be a helper function to see if newly added items exist in the cart already.
- Therefore will know how to handle the quantity inside in the cart.
- (i.e. - if product exists in the cart: plus quantity by 1, else add item to the cart)
-
-
-
Imported the
Cart-Item
compnent -
Imported the
useContext
&Cart.Context
-
From the CartContext I used destructuring to get the
cartItems
-
In the return, created a function to map over the cartItems π€π€π€
-
Wrapped the above in a ternary conditional using
cartItems.length
- if there is a length, return the above
- if there is no length, thus empty = return a message cart is empty
- added styling for empty cart message.
-
-
-
Imported
useContext
&Cart.Context
-
Added the
AddItemToCart
method via destructing fromCart.Context
-
In the Button,
- added onClick handler to call the
AddItemToCart
as a fucntion - passing the
product
through the function
- added onClick handler to call the
-
In Hindsight I took the above button function and made it a function called
addProductToCart
-
Then passed it into the onClick Handler (better for readability and optimization)
-
Click here to see more:
-
- Created a simple count of the items
- Created an empty array
- Created a for loop to loop over cartItems and extract the quantity and push to the empty array
- Using reducer to get the total of the array
This is working, however in hindsight I might want to be able to use this functionality in the checkout.(still to be built)
"If an item should be removed from the cart ( this functionality has not yet been implemented), the number should decrease."
It would be better if this functionality was in the cart.context. It could also be solved by using the useEffect Hook Back to the drawing board - lets undo this messy approach.
-
Because we are recounting the total quantity every time the
cartItems
changes, it makes sense to use the useEffect Hook.-
Imported useEffect
-
Added
cartItemCount
(default 0) to CartContext -
Added
[cartItemCount, setCartItemCount] = useState(0);
-
Using the useEffect
- dependancy =
[cartItems]
- created
count
using the reduce method total
+cartItem.quantity
setCartItemCount
using thecount
funciton
- dependancy =
-
Added the
cartItemCount
to the value to be passed into the provider
-
-
- Added
carItemCount
via destructuring - Using the above in the span inside the
ShoppingIcon
- Added
Click here to see more:
- [] The Checkout Button in the Cart.Icon-DropDown should take you to this new page
-
-
Created a simple
checkout.component.jsx
-
Created and imported an empty scss style sheet (updated with styling later )
-
In the main
App.jsx
;- imported the checkout component
- added a route to the new checkout component
-
In the
cart-dropdown
;- imported the
useNavigate
fromreact-dom-router
; - had to create a const that calls the useNavigate π€
- created a
gotToCheckoutHandler
to handle the navigation to new component - Used onClick method to call
goToCheckoutHandler
- imported the
-
Scaffolded the
checkout.component
return (productsm descruotion etc.) -
Added some basic styling to the scss
-
- Quantity Increase
- Quantity Decrease
- Total Price (Quantity x Price)
- Ability to remove the item entirely (Item and Quantity)
- Total Amount of all Items in the Cart.
-
π€ Need to create a way to pass the products in the cart into the Checkout... These items will need to be passed into a row/card sort of way...
What I am thinking is I can map over the cartItems and pass them into a checkoutCard component (thats imported in this checkout.component)
-
Creating a
CheckoutItem Component
to receivecartItems
and display them in theCheckOut-Component
page- Created a simple
checkout.component.jsx
- Created and imported an empty scss style sheet (updated with styling later)
- Created a simple
π€ I am going to neeed some new functions to handle the: - increasing and decreasing the quantity - removing the item from the cart
- Destructured the `cartItems` Objects (name, image etc.)
- Imported `CartContext`
- Destructured the function from `CartContext` (inlcuding newly created)
- Created handlers to pass `CartContext` funcitons into the return
- Finished creating the return
- Finished the basic styling for the component
-
-
removeItemToCart
(I want this to be named similaryly to theaddItemToCart
)- find the cart item to remove
- check if this is the last item
- if so remove entire item from cart
- If more than 1 of this item in cart, -return back cartitems with matching cart item with reduced quantity
-
clearItemFromCart
- Remove entire Item from the cart
-
updated the
CartContext
export -
updated the
CartProvider
export- updating
cartItemCount
andSetCartItemCount
tocartCount
andsetCartCount
- added
cartTotal
- minor naming updates to the
newCartCount
-useEffect
- added useEffect to handle the cost of the total cart
- minor naming updates to
addItemToCart
- added
removeItemToCart
- added
clearITemFromCart
- updated the values accordingly
- Getting the total cost of an item. (Quantity x Price)
- updating
-
-
- Imported
useContext
&CartContext
- Used
cartItems
fromCartContext
via destructuring
- Imported
-
- Updating the CartIcon with changes made to Context
Click here to see more:
-
- As a test run I am going to add a bit more mock data with different types (i.e. Mens, Jackets, Shoes etc.)
- Updating the
shop-data
json file - Reflecting changes in
products.context
(Temporarily breaks the shop page but after pushing the data up we will access the data via FireSotre)
-
Bringing in two new methods - collections - writeBatch
- Created a
addCollectionandDocuments
function- This is an async function that will take two params
key
- this wll be the name of the collectionobject
- this will be the data
- Obviously uses the
batch
andcollection
methods - As this could be a large file, it's broken into batches to be sent
- Await's a batch.commit() in return
- This is an async function that will take two params
- Created a
-
Imported the new
SHOP_DATA
-
Imported teh new
addCollectionandDocuments
-
Imported teh
useEffect
Hook -
Using the useEffect as I only want this to run literally once, thus commenting out after it's single use
- Passing in product-categories as the key
- Passing in SHOP_DATA as the object
- I realise this is not a normal way to push data but I just need to push some data up there one time and it gets the job done.
-
Saving - confirming the useEffect and function worked - it Did!
-
Commenting out the useEffect
-
Bringing in the querey method
-
Created a
getCategoriesAndDocuments
function- definining the
collectionRef
we want access too - using the
querey
on ourcollectionRef
- creating a const for the raw data
- manipulating the data to return in a format we can work with
- definining the
-
Imported the new
getCategoriesAndDocuments
function -
Created a useEffect Hook to fetch the data
- async function
- returns the categoryMap
- Checking the console log if everything worked
Click here to see more:
-
In the
ProductsContext
- renaminng the file from
ProductsContext
toCategoriesContext
- updating
main.jsx
to reflect change - updating the
shop.component
to reflect change (more required)
- renaming some of the exports - makes more sense now; - `ProductsContext` to `CategoriesContext` - `ProducstProvider` to `CategoriesProvider`
- updating functions to reflect `categoriesMap` as an object - updating return values to reflect name changes
- updating the useEffect Hook; - include `setCategoriesMap` function using (`CategoryMap`) - renaminng the file from
- updated to reflect name change of
ProductsContext
- Created a method to map through data from FireStore db.
- Used
Fragment
to wrap the entire return - Used
Object.keys
to turn keys into an array - Then mapped over the array to find the title
- Passed the tile into another
fragment
- created heading for category item
- copy pasted the old mapping function for each product
- Updated naming convention to utilise
categoriesMap
- Updated naming convention to utilise
- Used
Click here to see more:
This is how I will have the shop page display all the categories with a limited number of items ...
-
In the new
category-preview.component
;- Imported the
Product.Card
component andStylesSheet
. - Passing in the
{title, products}
- Created a heading div with just the text being clickable
- Creating the preview of products
- passed in the products
- using filter to decided what products we want
- use
_
to ignore product - use
idx
for the index of the prouduct idx < 4
so we only take in the first 4 products- then map through the remainging products and pas them into the
Product.Card Component
- Imported the
-
In the
shop.component
;- replaced the
ProductCard
import with the newcategory-preview.component
- replaced the
- Added
/*
as a wildcard to the end of the shop path
Created categories-preview
folder
- This will replace what was formly the shop.route
- Essentially a copy paste from the shop component
- Minor changes to make use of the
category-preview
component
Created category
folder
-
Created Style sheet blank, will complete later
-
Imported the
{ useParams }
hook fromreact-router-dom
-
destructuring
category
viauseParams()
hook -
Imported the
{ useContext }
hook fromreact
-
Imported the
CategoriesContext
in order to get thecategoriesMap
(i.e. all the data)
Initally I just got all the data from the
categoriesMap
const products = categoriesMap[category];
However this was causing an issue in that this was re-loading all the products on each re-render. This making things slow and sluggish - products would dissapear and have to reload.
The solution... Using the
useEffect
anduseState
hook'sThis way we can ensure the data will only rerender on our terms. Specifcally when the category changes or if the actual data changes (from
categoriesmap
)const [products, setProducts] = useState(categoriesMap[category]);
useEffect(() => { setProducts(categoriesMap[category]); }, [category, categoriesMap]);
Finally,
- Imported the
ProductContainer
- Added basic styling to the styles sheet
- Deleted almost all the no longer needed imports
- Imported
CategoriesPreview
&Category
components - Created paths for the different categories
path=':category' element={<Category />}
Click here to see more:
The error:
cannot read properties of undefined (reading 'map') in
catergory.components
The issue was that when our application mounts for the first time it is trying to load our categoriesMap
(i.e. the data from the back end )
Obviously this is async code and we are still waiting for the data to come through
Therefore we need to only load this data once it has arrived.
Luckily this was an easy fix after I figured out the issue,
-
In the return of the
catergory.components
;To ensure that products exists before we map over the products we can use
&&
and have the products -
Also updated the useStat hook to instead of being a default empty array, to infact include the
(categoriesMap[category])
Click here to see more:
The error:
cannot read properties of undefined (reading 'map') in
catergory.components
Because of how we named some of our classes and how the website has changed over time, currently we have some classnames that are clashing
Simple solution:
- Updated the naming convention of the
category-item
todirectory-item
- Updated the styling and classnames
- Updated corresponding references.
Click here to see more:
The problem:
category headings are not routing through to their corresponding path
Simple solution:
- Importing the Link method from react-router-dom
- Replacing the
span
withLink
- Passing in the title with backticks
- Minor updates to classnames and styling to prevent clashing
Click here to see more:
Styled-components Documentation
npm install styled-components
Now comes the fun of converting all the previouse scss files ....
- button
- cart-dropdown
- cart-icon
- cart-item
- category-preview
- Checkout-item
- directory
- directory-item
- form-input
- product-card
- sign-in-form
- sign-up-form
- navigation
- authentication
- categories-preview
- category
- checkout
- home
- shop
- index
This took way too long, however it served its purpose and now I have a very solid understanding of both SASS and Styled-Components and feel very comofortable using either or.
I still think tailwind might be my personal choice, especially in terms of time spent on implementation.
However, styled-components do have their advantages, particularly in providing more styling customization, that being said Tailwind does more than enough.
- Strictly converted the exisiting
.scss
files - Some other features/updates required (reffering to new components)
- Future updates to styling will be updated according to styled-components.
Click here to see more:
THE GOAL
The objective is to create an aesthetically pleasing and user-friendly interface that aligns with contemporary design trends while enhancing usability and engagement.
Major UI Update
Refactored the user interface of the website
Involved the following:
-
implementing substantial design enhancements to improve aesthetics, readability, and user experience.
-
Overhauled the visual design, optimized layout and typography, and enhanced usability.
-
Employed modern design principles, intuitive interactions, and visually appealing elements to elevate the website's visual appeal.
-
Optimized the information hierarchy to ensure a seamless user journey.
New Componenets added:
-
Footer
-
Front Splash
-
Image Carousel
-
About Page
Click here to see more:
THE GOAL
I really wanted to get a firm grasp on different state management techniques in React - Therefore I have gone through the steps of using and converting the following:
- useState & useEffect hooks
- useReducer's
- Redux-thunk
- Redux-saga
Next I will convert this enitre app (and Redux-Saga to use TypeScript)
Click here to see more:
Converted the cart to rather use reducers instead of useState and useEffect - Cart and Sign-in
Files touched in the process:
-
CartContext
-
UserContext
Created a reducer utils folder with a very simple helper function making reading and writing slightly easier
Click here to see more:
I have been given the advice to learn redux the hard way and not use the redux toolkit until I fully understand Redux.
I have been told that to fully understand what redux is doing I should implement using the legacy Redux way.
So that's what I will do - I will first convert this website to manage the state using Redux, then later I will do the same with Redux Toolkit
npm install redux react-redux and redux-logger
Creating a store folder to house all the redux related content
-
Setting up the root reducer & store.js
-
Creating the user store boiler plate code
-
Creating the categories boiler plate code
-
Creating the cart store boiler plate code
-
Updating the App.jsx and Main.jsx to utilize redux - replacing usercontext.
-
Hooking into and replacing references from useContext to useRedux
-
Updating Selectors for the cart state.
npm i redux-persist
-
modifications to our store.js file to use Redux Persist - replaced the value of the reducer property in the store from userReducer to persistedReducer
-
modifcation to the main.jsx, wrapped root component with PersistGate. This delays the rendering of your app's UI until your persisted state has been retrieved and saved to redux. NOTE the PersistGate loading prop can be null, or any react instance, e.g. loading={}
-
included the Thunk middleware, which will intercept and stop non-serializable values in action before they get to the reducer
-
passed our store as a parameter to persistStore, which is the function that persists and rehydrates the state. With this function, our store will be saved to the local storage, and even after a browser refresh, our data will still remain.
-
Specify how the incoming state is merged
-
Customize whatβs persisted
If I was using the Redux Toolkit package, there would be nothing to install - RTK's configureStore API already adds the thunk middleware by default. But because I am using the basic Redux createStore API and need to set this up manually, I first need to add the redux-thunk package:
npm install redux-thunk
- updated the Redux store to use the MiddleWare
- Created a function that makes an AJAX call to FireBase Server
- Updating the
Main.jsx
- Creating a loading-spinner componenet
- Updating the category actions and types types
- Rewriting the category reducer & updating selectors
- Implementing the loading animation in category componenets
Click here to see more:
npm install redux-saga
- Creating a
root-saga.js
file in thestore
- Importing and setting up
createSagaMiddleware
and theroot-saga.js
into the store.
Converting Categories State files into a Saga - Creating New Types - Creating New Actions - Updating Selectors & Reducers - Creating the Saga file
-
Setting up
cateforeis.saga.js
file -
Importing into the
root-saga
file -
Converting fetchCategoriesAsynch Function (Redux-Thunk) into a a Saga
-
Converting onAuthStateChanged Listener to Promise
-
Creating a single check, opposed to a listener checking the state every time user state updates
Converting User Session files into Saga - Creating New Types - Creating New Actions - Updating Selectors & Reducers - Creating the Saga file
- Updating the sign-in and sign-up component
- Updating the navigation componenet
Click here to see more:
npm install --save @stripe/react-stripe-js @stripe/stripe-js
-
Creating an Elements provider in the
main.jsx
-
Creating a
stripe.utils.js
file in the utils folder- importing loadStripe
- passing the publishable key (hiddent)
-
Creating a
.env
folder for the API keys -
Created a Local Neltify Development Server - for testing purpose
npm install netlify-cli -g
- NETLIFY CLI
-
Creating Netlify Functions folder
-
Creating a payment-form component (basic)
-
Building the basic layout of the component
-
Creating and Importing styling
-
Imported
CardElement, useStripe, useElements
fromstripe
-
Created payment handler
- preventDefault method
- ensure hooks are loaded before proceeded
- Call to API through netlify functions
- Alert for success of failure (temporary)
-
Using selectors to get:
- CartTotal (amount to pay)
- User's name (else defaults to guest)
-
Used useState for Processing Payment Animation
- Created loading animation on button
- Updated button component and styling
-
-
Rendering the componenet in the checkout section (temporary for testing)
Click here to see more:
Expand:
How to add typescript to an existing vite react app
Install dev dependencies
npm install -D typescript @types/react @types/react-dom
In packages.json, replace:
"build": "vite build"
With π
"build": "tsc && vite build"
Rename vite.config.js and main.jsx to vite.config.ts and main.tsx
Configure TypeScript by creating these two files in the root of your project:
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
tsconfig.node.json
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
Create a file named vite-env.d.ts
inside the src/ folder and copy and paste this
(with the three slashes at the beginning):
/// <reference types="vite/client" />`
In your index.html
you should change the name of your script from the old main.jsx
to main.tsx
like this:
<script type="module" src="/src/main.tsx"></script>
Expand:
How to Migrate a React App to TypeScript
Expand:
- Importing
AnyActon
from redux - Creating 'types' for ActionWithPayload and Action (without payload)
- Using Function overloading:
allows a single function name to have multiple definitions with different parameter lists or return types. It enables you to create multiple versions of a function that can handle different argument types or numbers.
- created the function createAction for the
- ActionWithPayload and
- Action (without payload)
- obviously have the function return
{ type, payload }
TYPE PROBLEM / BUG π
So coming accross an issue with categoriesReducer
and categories.actions
and that is that this is actually not 'type safe'.
Uncaught error: The slice reducer for key 'categories' retunred underfined durin initialization. If the state passsed to the reducer is undefined, you must acplicitly return the initial state. The iniitial state may not be undefined.
Further note: No error is being thrown , even if there is no default state
Solution : π
Need to extend the action creators so that they can be doing the type checking for us - using the .match method
Therefore in the reducer.utils
file :
- creating a "matcher" that will match the 'action creator' with the return type of the 'action' itself.
- using function overloading creating
withMatcher
functions -
- using function overloading creating
In the categories.actions
:
- Importing the
withMatcher
function and wrapping around each of the action creators - updating the switch casae (now using classical if conditionals)
This has been a really intense way to write this app and I have been told that redux and typescript can be the most challenging part in the front-end world as it requires a good understanding of various methadologies. For example - TypeGuards - .math method - Magical Types - Type Predicate Functions - Intersection and Return Types - Overloading Functions - etc...
Expand:
- Conveting the
categories.types
to.ts
:- using an enum type for the different action types
- creating a categories array type
- requires custom CategoryItem type - as an array
- Converting
categories.action
to.ts
:- Requires categories array type - created in & imported from the
categories.types
file - Import { CreateAction, Action and ActionWithPayload } from the
reducer.utils
file - Creating Types for the different ACTION_TYPES - depending on weather Action/ActionWithPayload
- Updating the code to make use of the newly created Types
- Reducer can only accept these Action_TYPES types
- therefore created a union type with these three Action's
- Requires categories array type - created in & imported from the
- Converting
categories.reducer
to.ts
:- Importing in the CategoryAction (union) from the categories.action:
- Discriminating union - what is this?
- Typing our the Categories state
- amending on what the switch case key is and using AnyAction to match the
action
with the differenttypes
- Converting
categories.selector
to.ts
:- importing
CategoriesState
fromreducer
, shape of the state reffering to category state. - TypeScrip will then infer the rest of the state calls
- Creating type for
CategoryMap
incategory.types
and importing - For the
selectCategoriesMap
:- Type the
categories
to use theCategoryMap
type - use
as CategoryMap
as a type assertion for the final resulting object
- Type the
- importing
I will convert the sagas at the end - this looks complicated.
Expand:
- Conveting the
cart.types
to.ts
:- using an enum type for the different action types
- creating a custom CartItem Type (extended from the CategoryITem from the categories.types)
- Conveting the
cart.actions
to.ts
:- Updating the helper functions - typing
- Creating types for the
setIsCartOpen
&SetCartItems
- Creating withMatch for
setIsCartOpen
&setCartItems
- Typing the variables in the Adding, Removing and Clearing Cart Items
- Conveting the
cart.reducer
to.ts
:- Importing relevant files
AnyAction
,CartItem
and thesetCartItems, setIsCartOpen
- Typing out the
CartState
- amending on what the switch case key is and using AnyAction to match the
action
with the differenttypes
- Importing relevant files
- Conveting the
cart.selector
to.ts
:- importing the
CartState
form theCartReducer
- Making minor amendments tot the createSelectors: "type"
- importing the
Expand:
- updating
addCollectionAndDocuments
:- Typing out the function
- Becuase
objectToAdd
could be an array of almost anything- creating a
ObjectToAdd
type with the one known item which is a title (as string) - then adding
<T extends ObjectToAdd>
- thus making our
objectToAdd
asT[]
- becuase this will be a promise that returns nothing giving it a type
promise<void>
- creating a
- updating the
getCategoriesAndDocuments
:- Typing out the function
- This function is returning our Category Data
- we have already created a type for this
Category
- importing this type from
category.types
file - Because this is a promise the type we get back will be
Promise<Category[]>
- While I know what data we are getting back from this 3rd-party-API (firebase)- TypeScript does not
- Therefore need cast the value
- return statement need to tell TS that returning a
Category
- return statement need to tell TS that returning a
- we have already created a type for this
- updating the
createUserDocumentFormAuth
-
for the
userAuth
so firebase actually gives us a type here calleduser
- import the
User
from firebase userAuth: User
- import the
-
additionalInformation
which in this website's current state is just a display name- creating a type
AdditionalInformation
- because displayname can be optional will use a
?
displayname? : string;
- because displayname can be optional will use a
- creating a type
-
This function is returning all the usersData OR nothing (logging out)
-
Firestore has/provides a typed
QueryDocumentSnapshot
- importing this type
- requires
<our custom Data Set>
-
Creating
UserData
type- createdAt, displayName, email
-
funciton will be return a promise of this data or nothing
-
include in the top
Promise<void | QueryDocumentsSnapshot<UserData>>
-
and casting the return of
userSnapshot as QueryDocumentSnapshot<UserData>
-
Handleing the catch.error
- cannot get the error.message without typing our the error.
- path of least resistance would just have the whole error printed out
-
- For the
createAuthUserWithEmailAndPassword
&signInAuthUserWithEmailAndPassword
functions- typing the inputs
- because this is returning a firebase type it already infers the type
Promise<UserCredential>
auto completed by firebase
- For the
SignOutUser
the same as the above it is inferred by FirebasesignOut(auth: Auth): Promise<void>
- For the
onAuthStateChangedListener
which is an observer function- Once again Firebase provides a type for this callback being
NextOrObserver
- Type definition for an event callback- import from firebase
- type the callback as this
NextOrObserver<User>
- our event being calledback is obviously the
<User>
- Once again Firebase provides a type for this callback being
- For the
getCurrentUser
fucntion that returns our user or nothing at all- fucntion returns
: Promise<User | null>
- fucntion returns
Expand:
- Conveting the
user.types
to.ts
:- creating an enum for all the different action type names
- Conveting the
user.actions
to.ts
:- Importing
AdditionalInformation
&UserData
- Importing
createAction
&Action
&ActionWithPayload
&withMatch
- now converting all the various actions
- typing the return statement
- typing params
- using withMatcher
- Importing
- Conveting the
user.reducer
to.ts
:- Importing relevant actions
- Typing out the
UserState
- amending on what the switch case key is and using AnyAction to match the
action
with the differenttypes
- Conveting the
user.selector
to.ts
:- importing the
CreateSelector
andUserState
- Adding the type state to the reducer and amending fixing the return to work with typescript
- Creating a
selectCurrentUser
Selector.
- importing the
Expand:
Root-reducer.js
--> TypeScript
- Very simple to do as everything is already typed
- Converting the file extention to
.ts
store.js
--> TypeScript
-
typing out the rootstate
- using
ReturnType<typeof rootReducer>
ReturnType
is because each one of these reducers are just functions- We cant simply just pass the
rootReducer
as this will result in an error- instead we need to get the type and if we look at the type of rootReducer its actually a combination of all the different reducers.
- using
-
Now we can export this RootState to all our other reducers (in the various selectors where we called state) and our
state
type will now be the newRootState
type.- importing the RootState type to the following:
- store
- cart
- user
- importing the RootState type to the following:
-
Getting a type error
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
- This happening because we are extending on the window type
- declare global with interface window withour extention
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
- This is an optional key so using a
?
in the statement - This is a type of compose and therefore
typeof compose
- This is an optional key so using a
- declare global with interface window withour extention
- This happening because we are extending on the window type
middleware
--> TypeScript
-
Import warning on 3rd party redux-logger
- Need to import the @types for the 3rd party redux-logger -
npm install @types/redux-logger
- Need to import the @types for the 3rd party redux-logger -
-
Typing out the Custom Middleware
import { Middleware } from "redux"
- > empty object in this caseimport { RootState } from '../store'
- Simply add a type to the Middleware using the newly imported states
Expand:
This was rather challenging at first but these two resources helped significantly:
TypeScript and Redux Sagas Implement Redux-Saga in your React TypeScript Project
Importing typed-redux-saga
& babel-plugin-macros
-
npm install --dev babel-plugin-macros
-
npm install @types/redux-logger
-
Fixing linting issues - got some help with this:
- In the
tsconfig.json
, under 'compiler' settings, adding the following to help handle error warnings:"downlevelIteration" : true,
- downlevelIteration explained
- In the
categories.saga.js
--> TypeScript
- Using
yield*
instead of the yield - How yield* works - typing my
errors as Erros
user.saga.js
--> TypeScript
- Using
yield*
same as above - Importing variouse
action
(types) and types from thefirebase.utils
- Involved quite a bit of typing and
- Had to add some conditionals to get everything to be type safe
- typing my
errors as Erros
Futher I found some missing types in my firebase.utils
and user.actions
, so had to fix those to get things working
Expand:
Converting styling.jsx
Adding the types library for styled components
- npm install @types/styled-components
In order of completion:
List of Components to Convert
- button
- form-input
- signin-form
- signup-form
- carticon
- cartdropdown
- cartitem
- product-card
- spinner
- footer
- payment-form
- categorypreview
- checkoutitem
- directory
- directoryitem
- front-splash
- carousel
List of Routes to Convert
- navigation
- category
- about
- authentication
- categories-preview
- checkout
- home
- shop
- Using useCallback
- Using useMemo
- Creating Dynamic Imports with: Suspense and Lazy