This repository serves as an online course for learning React. It’s here to help those who want to learn more about React and frontend in order to develop a SPA.
The best way of learning is by working on a real project.
Please take note that by learning React, you will not become a frontend developer. There are many other areas you should be able to handle.
The goal of this course is to give you a fundamental understanding of how to build a production-ready React web app.
The presenters demonstrated the best practices in building React applications — experience they gained by working on numerous projects.
This online course was created by utilizing material from around the web.
This course requires at least junior-level knowledge of programming, and relies on fundamental knowledge of React. Reading and understanding this amazing official documentation should be enough to get you started. But before that I suggest this course from Kent C. Dodds: The Beginner's Guide to React which is a little bit more updated because:
One thing to note is that the docs take a "class component" focused approach and are currently being rewritten to utilize newer standard practices for React called React Hooks.
General frontend knowledge is beneficial, but not necessary.
- Course Structure
- Lectures
- Used Technologies
- Credits
Please note that the course was designed to require a lot of self-studying during homework assignments.
We do recommend dedicating some extra time to studying the shared resources.
Goal: We are going to build together a dashboard (MVP of MVP :D). We choose dashboard as a bare minimum to display all needed parts.
MVP
- WIP
We will provide you with materials but the main activity is still up to you - learning and practising.
We will use this repository as the main source of truth.
Prerequisites
- basic HTML and CSS knowledge
- JavaScript knowledge
- Node.js and Yarn installed
- Code Editor or IDE (VS Code recommended)
HTML & CSS
It’s a bare minimum to build any website. There are many frameworks which are solving most of the issues, but still knowing the basics will boost your overall frontend knowledge. Pay attention to semantics and accessibility. A proper markup can improve User Experience.
- Learn to Code HTML & CSS - amazing course, worth reading even for experienced devs
- Overview of CSS concepts: https://www.taniarascia.com/overview-of-css-concepts/
- Accessibility resources:
- BEM methodology - one way to name things (we're using naming convention: two dashes style)
JavaScript
Since we are going to work with JavaScript like a lot, let’s be prepared for it!
- Install Node.js from the official website
- https://javascript.info/ - great resource for learning
- MDN Web Docs - documentation loved by JS community, search JS references on google (something like "js map mdn") and prefer MDN links
- video resources on egghead.io
- React docs
- Learn RegEx
General knowledge
- Git Cheat Sheet
- Browser support: https://caniuse.com/
- Visual Studio Code is recommended JavaScript code editor
- install recommended extensions (how to)
There are many different project starters for different purposes. Let's stick with the Vite.js.
- instant server start
- hot module reloading
- out-of-the-box support for TypeScript, JSX, CSS and more
- optimized build for production
- 0 configuration web application bundler - new features, bug fixes and security updates with a simple update of vite package
Config
Config should respect requirements during deployment and one of them can be the possibility to change API URL or other deployment-related variable. During frontend development you can access environment variables via process.env
or import.meta.env
(depends on the used bundler) but they are statically replaced during production. This means that for various deployments with different config variables you have to build FE part of the app again so you can statically replace deployment-related variables. But that's too heavy solution.
It would be much better to just reuse once built FE bundle and just replace these variables during runtime. This can be achieved externally by e.g. bash where you can build JSON file with env variables and this file has to be publicly available so you can fetch it within your app (client side).
If you insist on the best first load of your app then you can preload this JSON file in the HTML's <head>
via:
<link rel="preload" as="fetch" href="/env.json" crossorigin />
API
Before we start to implement features, let’s get the API communication ready. And before any implementation, you should get familiar with whichever API you are going to use.
In our case we try to simplify API. API will be done without any sync with the real one. But it can be synced via Swagger or similar standard.
Because we don't build BE we use Mock Service Worker (MSW.js) which intercepts requests on the network level so during development we can write our code, covering data fetching, as with the real API.
In addition to the temporary backup of the real API the MSW.js is also useful for testing and debugging.
Code Quality Tools
- Prettier - Opinionated Code Formatter, why Prettier?
- Building and enforcing a style guide
- It makes writing code more efficient!
- ESLint - The pluggable linting utility for JavaScript/TypeScript and JSX
- Linting is the process of running a program that will analyze code for potential errors. It's essential for programming. Linting helps you to catch bugs and follow best practices during development and therefore making production code more stable.
- Conventional commits - A specification for adding human and machine readable meaning to commit messages
- It makes it easier to write automated tools on top of!
- This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.
- Husky & lint-staged - Having previous tools without enforcing isn’t enough. Let’s use pre-commit hooks!
- husky: registers to Git hooks
- lint-staged: run linters on git staged files
Documentation
Documentation is a very important piece of any software.
- Write docs
- How to run your project
- Put there any necessary information how to build a project
- How to contribute to it (guideline)
- Anything else relevant
- Create a private repository
- Invite your mentor
- Open a Pull Request with Project Setup
- Project starter
- Config
- buildtime
- runtime
- API
- mock requests
- seed mock
- fetch simple data
- Code quality tools
- Documentation
- Read Main Concepts in React docs
React and it’s ecosystem gives users complete control over everything. But there are two common categories of grouping:
- Group by type
- (+) no mental overhead
- (-) becomes difficult to maintain, with growing gap
- Group by feature
- (+) follows rules of good project architecture (https://www.semanticscholar.org/paper/Clean-Architecture%3A-A-Craftsman%27s-Guide-to-Software-Martin/4e0e958168e6390a26493e2ba599f454de1dfdc2?p2df)
- (-) you have to think about it in advance
- (-) sometimes refactoring is necessary
This projects is more inclined to the second option. Each feature has everything it needs to work on its own, such as its own styles, assets...
Routing is the root mechanism of user interaction on the web.
Main functions from user's point of view are:
- Navigation
- Sharing
- Bookmarking
Two types of routing:
- Server-side
- When browsing, the adjustment of a URL can make a lot of things happen. This will happen regularly by clicking on a link, which in turn will request a new page from the server. A whole new document is served to the user. Page is refreshed.
- Client-side
- The route is handled internally by the JavaScript.
- Routing between views is generally faster, because less data is processed.
- It’s more seamless, since there is no page refresh and things like animations are easier to implement.
Hash-based routing
- example:
https://site.com/#/products
- Anchor part of the URL to reflect state of the application
- Hash changes don’t trigger page reload
- Unintuitive
- Provides methods for manipulating the browser’s history
window.history.back()
window.history.forward()
window.history.pushState()
A common solution to client-side routing with React is the library react-router
which seamlessly integrate History API within React application.
We have a WIP . Before we move further, let's implement some decent styles, so that the app is more pleasant to work with.
There are numerous ways how to style web app:
- CSS (Sass) via classes (naming convention recommended - e.g. BEM, OOCSS, SMACSS, ...)
(+)
complete control over styling(+)
you can choose prefered naming conventions(-)
requires high experience with styling so you know how to structure CSS/Sass files, hot to use properly CSS variables(-)
styles are global so you need a good way how to name and encapsulate component classes (BEM recommended)(-)
you have to found and define standards and conventions(-)
requires custom solution for accessibility, component composition, keyboard navigation, style overrides, etc.
- CSS frameworks
- Bootstrap
(+)
huge community(+)
customizable(-)
global CSS(-)
custom naming conventions(-)
historically influenced by jQuery(-)
only CSS/Sass with plain JS
- atomic classes (Tailwind CSS)
(+)
framework agnostic(+)
CSS doesn't increase its size at one point because you reuse already existing styles(+)
you don't need to care about naming (you try to use low level classes directly in components)(-)
lang-in-lang (you are working with atomic classes which are just a thin layer between CSS properties and components) - basically you have whole styling in one huge string which doesn't offer any useful intelligent tool from CSS files (auto suggestion, linting, media queries, ...)(-)
logic required for customazibility of design system components is difficult (you are working with strings all the time)(-)
requires custom solution for accessibility, some components requires JS for a proper behavior(-)
responsive styles in tailwind requires a combination of pseudo-classes. This can get rather lengthy as your project grows. The API:<img class="w-16 md:w-32 lg:w-48" src="...">
- Bootstrap
- CSS-in-JS (styled-components, emotion, Stitches ...)
(+)
framework agnostic(+)
component styles are isolated(-)
if you need friendly CSS classnames then you need to set bundler (not all of them support this feature)(-)
increase bundle size (common libraries require JS runtime for now...there are PoCs with compilation to static CSS files but it's usually tightly coupled with specific bundler)(-)
at one point it can decrease performance (only withing huge applications - dashboards are not one of them)(-)
requires custom solution for accessibility, some components requires JS for a proper behavior
- themeable React components (Chakra UI, Material UI, ...)
- Chakra UI
(-)
only for React(+)
provides the convenience of Tailwind and all these other benefits (handles semantic html structure, meeting the WAI-ARIA requirements, keyboard navigation, etc.) out the box(+)
accessible(+)
themeable(+)
composable(+/-)
uses CSS-in-JS under the hood (emotion
+styled-system
) - small price to pay when it comes to runtime- This runtime footprint is caused by style computations by
styled-system
, and className generation byemotion
. - If your app deals with high, frequently changing data that is performance sensitive, you might notice this footprint as your app grows.
- This runtime footprint is caused by style computations by
(-)
built only for React apps
- Chakra UI
- web components (~ JS framework agnostic) (Ionic)
- Ionic
(+)
framework agnostic(+)
CSS + JS(+)
isolated components (CSS + JS)(-)
immature web components (problem with Shadow DOM during testing or debugging)(-)
long-standing issues(-)
poor framework focused docs (React is not the main JS adapter)
- Ionic
- OSS company-based design systems (IBM Carbon, Microsoft Fluent UI - theme designer, Adobe Spectrum, Material UI,...)
(+)
extensive API docs(+)
extensive design docs (do's & dont's)(+)
backed by huge players(-)
very specific design(-)
non-fully-customizable(-)
some of them are dinosaurs (class based React components, custom non-react logic, weird API usage)
- unstyled React components (Radix UI, Reach UI)
(+)
CSS-solution agnostic (you can use raw CSS, Sass, CSS-in-JS, CSS modules,...)(+)
solves accessibility and component-related JS logic(-)
CSS architecture is on your own(-)
currently immature libraries- example how to use Radix UI primitives with CSS-in-JS library - Stitches: https://github.com/radix-ui/design-system (look into the
./components/
folder)
If you do multiple products with the same feeling then you should consider to use design system which is not only about CSS & HTML but it should also cover design decisions and best practices.
It's a good practice to have some showroom with all UI components. The common solution to this is Storybook.
- Set up React router
- Think about and implement the rest of the project architecture
- Fetch and display WIP
- Add styles for WIP
Managing state is arguably the hardest part of any application. It's why there are so many state management libraries available and more coming around every day (and even some built on top of others... There are hundreds of "easier redux" abstractions on npm). Despite the fact that state management is a hard problem, I would suggest that one of the things that makes it so difficult is that we often over-engineer our solution to the problem. -- Kent C. Dodds (source)
How to think about React and state:
-
don't break up component into multiple components prematurely
-
use state colocation (Separate your state into different logical pieces rather than in one big store, so a single update to any part of state does NOT trigger an update to every component in your app. ) source: Kent C. Dodds Blog
-
use React context for simpler global state (rather static values with few updates)
-
use
react-query
/useSWR
for server state (solves client caching, memoizing query results, background updates, deduping multiple requests for the same data into a single request,...) -
in some scenarios it's useful to have a global state management but be aware of the all other solutions so you know when you need it:
- Redux/zustand - flux principles
- jotai/Recoil - atomic principles
- The state is split into atoms, which are much smaller and lighter than something like a redux store. They are created with
atom
functions and could be created independently from each other or on-demand. This allows for easier code-splitting. - Compared to regular React Context with
useState
, atoms can be used for high-frequency updates.
- The state is split into atoms, which are much smaller and lighter than something like a redux store. They are created with
- MobX/Valtio - proxy state
On the other side you can use global state management (like Redux, MobX, zustand, React Context...). It allows to subscribe/observe the state somewhere in the tree without any notice to their parent.
Out of the box, React applications do not come with an opinionated way of fetching or updating data from your components so developers end up building their own ways of fetching data. This usually means cobbling together component-based state and effect using React hooks, or using more general purpose state management libraries to store and provide asynchronous data throughout their apps.
While most traditional state management libraries are great for working with client state, they are not so great at working with async or server state. This is because server state is totally different. For starters, server state:
- Is persisted remotely in a location you do not control or own
- Requires asynchronous APIs for fetching and updating
- Implies shared ownership and can be changed by other people without your knowledge
- Can potentially become "out of date" in your applications if you're not careful
Once you grasp the nature of server state in your application, even more challenges will arise as you go, for example:
- Caching... (possibly the hardest thing to do in programming)
- Deduping multiple requests for the same data into a single request
- Updating out of date data in the background
- Knowing when data is "out of date"
- Reflecting updates to data as quickly as possible
- Performance optimizations like pagination and lazy loading data
- Managing memory and garbage collection of server state
- Memoizing query results with structural sharing
If you're not overwhelmed by that list, then that must mean that you've probably solved all of your server state problems already and deserve an award. However, if you are like a vast majority of people, you either have yet to tackle all or most of these challenges and we're only scratching the surface!
-- Tanner Linsley (source)
- WIP feature on WIP
- Use
react-query
/useSwr
for fetching data
Forms can be incredibly complex in React. We lose a lot of what the browser does by default as far as form validations and error reporting which leads to a lot of people writing a large amount of boilerplate.
Usually the problems fall into one of three categories:
Without the aformentioned baked-in browser functionality, we are left with writing large amounts of code to do very simple checks and balances such as making sure a form is populated or that an email address matches a valid pattern.
This often leads to a series of conditionals, often nested conditionals that make managing the state of the form more complex.
Once the user submits, you now need to have access to your data inside you submit handler, right? This often takes one of two shapes:
Assigning a reference to each element and then accessing data through those references, often requiring a large number of references to be created at the top of the function to be used later.
Creating a controlled component where the value of the inputs is derived by state and each keystroke updates the state of each input, requiring a large number of useState calls or a single JavaScript object that requires frequent cloning of the state to update a single piece on change.
With buggy validation, comes buggy error reporting where it is not uncommon to have multiple pieces of state toggling error messages off or a more complicated approach of something like an array of errors that must be mapped and rendered into the DOM. This is often handled completely manually and by hand on each input for each set of errors / validation rules.
There are a few libraries that aim to solve this issue and make forms more accessible. We will be looking at
react-hook-form
which I recently discovered and fell in love with but another popular option is Formik.These libraries seek to internalize these common issues and give a more declarative approach to building forms in React.
-- Jody LeCompte (source)
We try react-hook-form
.
- Short video tutorial with
react-hook-form
- Article: Why react-hook-form is my new favorite form library
Types of Flows:
- Server-side Sessions
- JSON Web Tokens
Server-side Sessions
- Client receives session id from the server
- Server has to make a record in a database
JSON Web Tokens
- Open standard that defines a way for securely transmitting information between parties as a JSON object
- header + claims + signature = JWT
- The most common scenario is authorization
- Access tokens
- Refresh tokens
- debugger example
Types of Attacks
- Cross-site scripting (XSS)
- Type of vulnerability where attacker is able to inject JavaScript that will run on your page.
- React JSX Prevents Injection Attacks
- Cross-Site Request Forgery (CSRF or XSRF)
- Type of vulnerability that exploits how browser works with cookies.
- A cookie can only be sent to the domains in which it is allowed
- The cookie will be sent for a request regardless of whether you are on bank.com or i.hacked.you
- Type of vulnerability that exploits how browser works with cookies.
Where to store Tokens
Choosing the right storage depends on the use-case, therefore you need to be aware of trade-offs you make.
Three options:
-
Web Storage (
localStorage
orsessionStorage
)-
By leveraging Web Storage API, which provides mechanism to store key/values pairs in a developer friendly way.
-
(+)
Immune to CSRF -
(+)
Accessible only by JavaScript running on the same domain that created data -
(+)
Allows to use the most semantically correct approach -
(+)
It’s easy to authenticate specific requests -
(-)
Vulnerable to XSS -
(-)
Cannot be accessed by Javascript running in a sub-domain -
(-)
Only browser can perform authenticated requests
-
-
HTTP-only Cookie
- The actual work is handled on the server, so you don't need to do anything client-side as the browser will automatically take care of things for you.
(+)
Immune to XSS(+)
The browser automatically includes the token in any request that meets the cookie specification(+)
Can be created at a top-level domain and used by subdomains(+)
More options available for full control (httpOnly
,secure
,sameSite
...)(-)
Vulnerable to CSRF(-)
It’s harder to authenticate specific requests(-)
Possible usage of the cookies in subdomains(-)
Semantically incorrect
-
JS accessible cookie
-
By leveraging
document.cookie
browser API, which is not very developer friendly as Web Storage, therefore you will need to parse it yourself. -
(+)
Immune to CSRF -
(+)
Can be created at a top-level domain and used by sub-domains -
(+)
Allows to use the most semantically correct approach -
(+)
It’s easy to authenticate specific requests -
(-)
Vulnerable to XSS -
(-)
Only browser can perform authenticated requests
-
There are several authentication services out there such as Firebase Authentication, Auth0, etc. In sum project, you might need more control over your authorization and user management with advanced features.
That’s where Keycloak is an excellent choice, It provides lots of features out of the box. Keycloak has built-in support for OpenID Connect and SAML 2.0 as well as a number of social networks such as Google, GitHub, Facebook, and Twitter.
We walk through the following steps:
- Install Keycloak - Get Started with Keycloak Docker
- Setup Realm, Client ID in Keycloak
- Install
keycloak-js
- Setup Keycloak instance as needed
- Routes restrict based on Keycloak Authentication & Authorization
- Complete WIP form
Thanks belongs to the whole Open Source community for making it possible to create this type of course!