Sept 25th
Front-End Engineering in 2024 & Mastering React
Oct 2nd
Building Large Scale Web Apps
Oct 9th
Building a Personal Brand & Breaking Into FAANG
Oct 16th
Leveling Up From Junior To Senior To Staff
Sessions will happen 7:30PM — 9:00 PM (Wednesdays)
Q&A
(Dedicated Q/A periods)
Recordings
(Will be shared on Slack)
Exercises
(to work on between sessions)
You can reach out to me anytime on Slack through-out the 4 weeks!
Happy to answer questions/help where I can async.
Who's part of the cohort?
Designers/Creative Directors
Backend Engineers
Fullstack Engineers
Junior Frontend Engineers
Senior Frontend Engineers/Team Leads
I want to make this workshop as helpful to everyone as much as possible.
Houtan
Borys
today's folks
Navyatha
Menna
Anthony
Bilal
Q/A!
Any questions?
- JavaScript
- React
- Common and important concepts in React
- Data Fetching
- React + TypeScript
- Frameworks (Vite, Remix, Next.js)
- React 19, RSCs, and new APIs
- Front-End Engineering in 2024
- Mobile Responsiveness
- Accessibility
- User Experience (UX) & Interaction Design
- Performance Optimization
- Web Security
- API Integration
many different focus areas
- Infra/build-tooling
- Front-End Engineer
- Front-End Engineer
- Product Engineer
- Design Engineer
- UI/UX Engineer
- Web Developer
- Creative Developer
<form>
<label for="name">Name:</label>
<input type="text" id="name" name="name" />
<label for="email">Email:</label>
<input type="email" id="email" name="email" />
<input type="submit" value="Submit" />
</form>HTML
form {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background: #f9f9f9;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
/* ... */CSS
JavaScript is often used for one of two things — Interactivity & Dynamically updating content.
This could involve changing elements on the page in response to user actions, such as form submissions, button clicks, or real-time data fetching (e.g., updating live scores or news feeds).
<form>
<label for="name">Name:</label>
<input type="text" id="name" name="name" />
<label for="email">Email:</label>
<input type="email" id="email" name="email" />
<input type="submit" value="Submit" />
</form>let form = document.querySelector('form');
let nameInput = form.querySelector('#name');
let emailInput = form.querySelector('#email');
form.addEventListener('submit', function(e) {
e.preventDefault();
let name = nameInput.value;
let email = emailInput.value;
console.log(`Name: ${name}, Email: ${email}`);
});function Form() {
// creating state object
const [formData, setFormData] = React.useState(
{ name: "", email: "" }
);
// handle change in our form inputs
function handleChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
// handle form submit
function handleSubmit(e) {
e.preventDefault();
console.log(formData);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" onChange={handleChange} value={formData.name} />
<label htmlFor="email">Email:</label>
<input type="email" id="email" name="email" onChange={handleChange} value={formData.email} />
<input type="submit" value="Submit" />
</form>
);
}
Declarative Programming
React
TypeScript
Lodash
Vite
Redux
Tailwind
HTML
CSS
JavaScript
compiled
With standard JavaScript and HTML, we used query selectors and event listeners to add interactivity to our form.
With React, we achieve this by building our JavaScript logic into our component since components allow us to couple HTML and JavaScript together. In other words, we use a more declarative approach to building out the user interface (UI)!
- State
- Components
- Props
- Lifecycle
- Hooks
- Component re-rendering
- Frameworks (Vite, Remix, Next.js)
- React 19, RSCs, and new APIs
- Data Fetching
- React + TypeScript
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}import { useState } from 'react';
function CounterButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
export default function App() {
return (
<div>
<CounterButton />
</div>
);
}
import { useState } from 'react';
function CounterButton({ count, setCount }) {
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<CounterButton
count={count}
setCount={setCount}
/>
</div>
);
}Any time state or props change in a component, React re-renders the component. This re-rendering is triggered so that the component can display the most up-to-date data in the UI.
React uses an internal algorithm called reconciliation to efficiently update only the parts of the DOM that have changed, rather than re-rendering the entire UI.
In React, a component's lifecycle refers to the series of stages it goes through from creation (mounting), updating (when state or props change), to removal (unmounting) from the DOM.
The useEffect hook allows you to run side effects at specific stages of this lifecycle, such as after the component mounts, updates, or unmounts
import { useState, useEffect } from 'react';
function CounterButton() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
} useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);import React, { useEffect } from "react";
export const FunctionComponent = () => {
useEffect(() => {
console.log("run for every component render");
});
return (
// ...
);
}Run effect on every render
import React, { useEffect } from "react";
export const FunctionComponent = () => {
useEffect(() => {
console.log("run only for first render");
}, []);
return (
// ...
);
}Run Effect Only on First Render (i.e., on mount)
import React, { useState, useEffect } from "react";
export const FunctionComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(
`run for first component render
and re-run when 'count' changes`
);
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Click to increment count and trigger effect
</button>
);
};Run Effect on First Render and Rerun When Dependency Changes
State → useReducer
Props → Context
Components → Server-Side Rendering (SSR) & SSG
State → Redux
State + Effect → Custom Hooks
Components → React.lazy / Suspense
HTTP Requests (REST APIs)
GraphQL
WebSockets
Server-Sent Events (SSE)
Realtime Database / Firestore (e.g. Firebase)
gRPC
GET
POST
PUT
DELETE
import { useEffect } from "react";
const App = () => {
// ...
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
"https://api.example.com/data",
);
if (!response.ok) {
throw new Error(
"Network response was not ok",
);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error(
"There was a problem:",
error.message,
);
}
}
fetchData();
}, []);
// ...
};import { useEffect } from "react";
import axios from "axios";
const App = () => {
// ...
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get(
"https://api.example.com/data",
);
console.log(response.data);
} catch (error) {
console.error(
"There was a problem:",
error.message,
);
}
}
fetchData();
}, []);
// ...
};import { useEffect, useState } from "react";
import axios from "axios";
const App = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await axios.get("https://api.example.com/data");
setData(response.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>Fetched Data:</h1>
<pre>{data.information}</pre>
</div>
);
};Initial State
Fetch data and set state properties
render template based on state
This encompasses a very large part of web/UI development
- Make API requests
- Show loading/error indicator states
- Show data when available
More sophisticated data-fetching libraries
More sophisticated data-fetching libraries
Libraries that provide Hooks, caching, synchronization, etc., to make data-fetching easier for larger web applications.
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchTodoList = async () => {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos",
);
return response.data;
};
export function App() {
const { data, isLoading, isError } = useQuery(
{
queryKey: ["todos"],
queryFn: fetchTodoList,
},
);
// ... further code to display/utilize the data
}export function App() {
const { data, isLoading, isError } = useQuery(
{
queryKey: ["todos"],
queryFn: fetchTodoList,
},
);
if (isLoading) {
return <p>Request is loading!</p>;
}
if (isError) {
return <p>Request has failed :(!</p>;
}
return (
<div>
<h1>Todos List</h1>
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}Fetch data
render template based on state
const {
data,
dataUpdatedAt,
error,
errorUpdatedAt,
failureCount,
failureReason,
fetchStatus,
isError,
isFetched,
isFetchedAfterMount,
isFetching,
isInitialLoading,
isLoading,
isLoadingError,
isPaused,
isPending,
isPlaceholderData,
isRefetchError,
isRefetching,
isStale,
isSuccess,
refetch,
status,
} = useQuery(
{
// options
},
queryClient,
);const {
// ... returnsd
} = useQuery(
{
queryKey,
queryFn,
gcTime,
enabled,
networkMode,
initialData,
initialDataUpdatedAt,
meta,
notifyOnChangeProps,
placeholderData,
queryKeyHashFn,
refetchInterval,
refetchIntervalInBackground,
refetchOnMount,
refetchOnReconnect,
refetchOnWindowFocus,
retry,
retryOnMount,
retryDelay,
select,
staleTime,
structuralSharing,
throwOnError,
},
queryClient,
)
Caching
Caching
Caching
Developer Tools
- Design your data model well
- Optimize your endpoints
- Batch requests where applicable
- Prioritize and defer
- Use lazy-loading
- Leverage caching
- Evaluate the use of GraphQL
- Monitor/analyze performance
[Backend]
[Backend]
Q/A + 10 min Break!
Any questions?
let variable = 5
// ...
variable = "5"let variable: number = 5
variable = "5"Type 'string' is not assignable to type 'number'
- Better Developer Experience
- Easier Maintenance
- Better Collaboration
Typed Props
React Hooks
Type Inference
Declaration Files
Autogen API types
Typed Props
interface Props {
name: string;
age: number;
}
const Person = ({ name, age }: Props) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
)
}<Person name={false} age={[30]} />Type 'boolean' is not assignable to type 'string'Type 'number[]' is not assignable to type 'number'React Hooks
const [name, setName] = useState<string>("Hassan")(newValue: string) => voidstringReact Hooks
type Theme = 'light' | 'dark'
const ThemeContext = createContext<Theme>('light')
const ThemedComponent = () => {
const theme = useContext(ThemeContext)
}'light' | 'dark'React Hooks
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): UseFetchResult<T> {
// ...
// ...
return { data, loading, error }
}React Hooks
type Data = { id: string, name: string }
const App = () => {
const { data, loading, error } = useFetch<Data>("https://example.com")
// ...
}Data | nullbooleanstring | nullType Inference
const [name, setName] = useState<string>("Hassan")(newValue: string) => voidstringType Inference
const [name, setName] = useState("Hassan")(newValue: string) => voidstringType Inference
const [name, setName] = useState<string | null>("Hassan")Declaration Files
yarn add <package_name>Declaration Files
yarn add lodashDeclaration Files
import { uniq } from 'lodash'
const App = () => {
const uniqueArr = uniq([2, 1, 2]);
return (
//...
);
}Cannot find module 'lodash' or its corresponding type declarations.const uniqueArr: anyDeclaration Files
declare module 'lodash' {
export function chunk<T>(array: T[], size?: number): T[][];
export function flatten<T>(array: T[][]): T[];
export function uniq<T>(array: T[]): T[];
// ...
}lodash.d.tsDeclaration Files
Declaration Files
yarn add -D @types/lodashDeclaration Files
import { uniq } from 'lodash'
const App = () => {
const uniqueArr = uniq([2, 1, 2]);
return (
//...
);
}const uniqueArr: number[]Declaration Files
declare module 'lodash' {
export function chunk<T>(array: T[], size?: number): T[][];
export function flatten<T>(array: T[][]): T[];
export function uniq<T>(array: T[]): T[];
// ...
}lodash.d.tsDeclaration Files
{
// ...
"include": [
"./path/to/declarations/*.d.ts",
// other paths
],
// ...
}tsconfig.jsonAuto-generating types from APIs
type User {
id: ID!
name: String!
}
type Query {
getUser(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}GraphQL
Auto-generating types from APIs
interface UserData {
getUser: {
id: string;
name: string;
};
}
interface UserVariables {
id: string;
}
const UserComponent: React.FC<UserProps> = ({ userId }) => {
const { data, loading, error } = useQuery<UserData, UserVariables>(GET_USER, {
variables: { id: userId },
});
};GraphQL
Auto-generating types from APIs
GraphQL
Auto-generating types from APIs
GraphQL
Auto-generating types from APIs
GraphQL
type User {
id: ID!
name: String!
}
type Query {
getUser(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}type User = {
__typename?: 'User';
id: string;
name: string;
};
type Query = {
__typename?: 'Query';
getUser: User | null;
};
type Mutation = {
__typename?: 'Mutation';
createUser: User | null;
};Auto-generating types from APIs
REST
openapi: 3.1.0
# ...
paths:
/user:
get:
operationId: getUser
parameters:
- name: id
schema:
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
post:
operationId: createUser
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
# ...Auto-generating types from APIs
REST
Auto-generating types from APIs
REST
openapi: 3.1.0
# ...
paths:
/user:
get:
operationId: getUser
parameters:
- name: id
schema:
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
post:
operationId: createUser
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
# ...type User = {
__typename?: 'User';
id: string;
name: string;
};
type Query = {
__typename?: 'Query';
getUser: User | null;
};
type Mutation = {
__typename?: 'Mutation';
createUser: User | null;
};- Ensure all component props are typed
- Utilize React Hooks with proper types
- Leverage TypeScript’s type inference
- Import and use declaration files
- Rely on tools that auto-generate API types
Frameworks help streamline the development process by providing tools and optimized environments for building faster, scalable, and modern web applications.
<!DOCTYPE html>
<html>
<head>
<title>React Example</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"
></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
ReactDOM.render(
<h1>React in a simple HTML file!</h1>,
document.getElementById("app")
);
</script>
</body>
</html>
- Cumbersome to deal with JSX
- Lack of optimization (tree-shaking, code-splitting, etc.)
- Lack of support on Server-side rendering, TypeScript, Testing, etc.
- Poor maintainability
- We want JavaScript modules/components in their own files
Packages/npm/yarn/etc.
Optimized build/ step
TypeScript
HMR (Hot Module Replacement)
- Vite: A fast build tool focused on rapid development and hot module replacement, perfect for small-to-medium projects and prototyping.
- Next.js: A React-based framework offering server-side rendering and static site generation, ideal for SEO-optimized, large-scale applications.
- Remix: A full-stack framework with a focus on performance and user experience, excelling at building highly interactive and data-driven applications.
A large part of how we use web applications today involves interacting with forms for various purposes, from user authentication to data submission to e-commerce transactions, feedback collection, search queries, and much more.
import React, { useState, useCallback } from 'react';
const submitForm = async () => {
// form submit logic here
};
export function Component() {
// create form state
const [formState, setFormState] = useState(null);
const [isPending, setIsPending] = useState(false);
// handle form submission
const formAction = useCallback(async (event) => {
event.preventDefault();
setIsPending(true);
try {
const result = await submitForm();
setFormState(result);
} catch (error) {
setFormState({ message: 'Failed to complete action' });
}
setIsPending(false);
}, []);
// display form template
return (
<form onSubmit={formAction}>
{/* Form Template */}
</form>
);
}In React 18, this concept of transitioning the UI from one view to another in a non-urgent manner was given the name of transitions.
In React 19, the concept of transitions is taken a step further as functions that use async transitions are now referred to as Actions.
import { useActionState } from "react";
const submitForm = async (formData) => {
/* form submit fn that calls API */
};
const action = async (currentState, formData) => {
try {
const result = await submitForm(formData);
return { message: result };
} catch {
return { message: "Failed to complete action" };
}
};
export function Component() {
const [state, dispatch, isPending] = useActionState(
action,
null
);
// ...
}import { useActionState } from "react";
const submitForm = async (formData) => {
/* form submit fn that calls API */
};
const action = async (currentState, formData) => {
try {
const result = await submitForm(formData);
return { message: result };
} catch {
return { message: "Failed to complete action" };
}
};
export function Component() {
const [state, dispatch, isPending] = useActionState(
action,
null
);
return (
<form action={dispatch}>
{/* ... */}
</form>
);
}
import { useActionState } from "react";
const submitForm = async (formData) => {/* ... */};
const action = async (currentState, formData) => {/* ... */};
export function Component() {
const [state, dispatch, isPending] = useActionState(
action,
null,
);
return (
<form action={dispatch}>
<input type="text" name="text" disabled={isPending} />
<button type="submit" disabled={isPending}>
Add Todo
</button>
{state.message && <h4>{state.message}</h4>}
</form>
);
}
Unlike traditional React components, which run solely in the browser, React Server Components allow for a seamless integration of server-side rendering into the React architecture.
Server-side rendering
Client-side rendering
All the rendering happens on the client
Server-side rendering
Server-side rendering
Server-side rendering
React Server Components are a new capability in React that allows us to create stateless React components that run on the server.
import db from "./database";
// React Server Component
async function BlogPost({ postId }) {
// Load blog post data from database
const post = await db.posts.get(postId);
// Load comments for the post from database
const comments = await db.comments.getByPostId(postId);
return (
<div>
<h2>{post.title}</h2>
<p>{post.content}</p>
<h3>Comments</h3>
<ul>
{comments.map((comment) => (
<li key={comment.id}>
<Comment {...comment} />
</li>
))}
</ul>
</div>
);
}
Since Server Components are run on the server and not the browser, they’re unable to use traditional React component APIs like useState.
// React Client Component
"use client"
export function Comment({ id, text }) {
const [likes, setLikes] = useState(0);
function handleLike() {
setLikes(likes + 1);
}
return (
<div>
<p>{text}</p>
<button onClick={handleLike}>Like ({likes})</button>
</div>
);
}
Using RSCs currently requires a compatible server and client environment, which often means relying on a framework. For a significant period of time, Next.js was the only framework supporting RSCs, paving the way for their adoption.
- JavaScript
- React
- Common and important concepts in React
- Data Fetching
- React + TypeScript
- Frameworks (Vite, Remix, Next.js)
- React 19, RSCs, and new APIs
- Front-End Engineering in 2024
- Design Systems & Component Libraries
- State Management
- Internationalization
- Organizing Code
- Personalization & A/B Testing
- Testing
- Deployment
What's coming next week?
#1 Spin up a Vite / React Project
Take-home coding exercises
- Ensure you have Node/npm installed
- Build out a simple React component
- Use State
- Counter/Todo Input/Form
#2 Interact with an API
- Interact with an API when component mounts for the first time OR after an action is taken
#3 Build up from 1) and 2)
- Try out TypeScript (if you haven't before)
- Try a framework/library you haven't tried before
- Next.js
- Remix
- Try out React 19 (in beta)