Client
Server
Database
HTTP
browser
request
html page
Client
Server
Database
HTTP
React in browser, mobile app...
API
request
data
Single page application
Web server
html, js
npm create vite@latestProject name: … (react-course)
Select a framework: › React
Select a variant: › Typescript
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)npm run dev
const label = React.createElement('a', {
href: 'https://google.com'
}, 'Go to Google.com');<a href="https://google.com">Go to Google.com</a>children
props
type
import React from 'react';
function App() {
return (
<div className="App">
Hello
</div>
);
}function App() {
return React.createElement('div', { className: 'App' }, 'Hello');
}import React from 'react';
function App() {
return (
<div className="App">
Hello
</div>
);
}function App() {
return React.createElement('div', { className: 'App'}, 'Hello');
}import React from 'react';
function App() {
return (
<div>
Yes
</div>
<div>
No
</div>
);
}function App() {
return ????????
}import React from 'react';
function App() {
return (
<>
<div>
Yes
</div>
<div>
No
</div>
</>
);
}function App() {
return <h1>Hello</h1>;
}
function App() {
let something = 'hello';
return <div>{something}</div>;
}function Array() {
let array = [1,2,3];
return <div>
{array.map((item, index) => <span key={index}>{item}</span>)}
</div>;
}
‼️ key prop
type Props = {
name: string;
};
function NameComponent(props: Props) {
return <h1>Hi, my name is {props.name}!</h1>;
}
ReactDOM.render(
<NameComponent name="Martin" />,
document.getElementById('root')
);Component
Component
Component
Component
Component
Component
Component
User info
ArticleList
Article
Today Weather
Article
I am smart 💡
function NameComponent(props) {
return <h1>{props.name}</h1>;
}function App() {
return <NameComponent name="Martin" />
}<button type="button" onClick={() => console.log('Hello')}>
Hello world
</button>| 1 | 2 | 3 |
|---|---|---|
| 4 | 5 | 6 |
| 7 |
const input = [1,2,3,4,5,6,7]
<Table columns={3} array={input} />| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|
1️⃣
const input = [1,2,3,4,5,6,7]
<Table array={input} />2️⃣
Tatranka
Fidorka
Mars
Price: 20,-
Coins: 50,-
🪙
Insert coin
3x
5x
0x
out of stock
Buy
Tatranka
Fidorka
Mars
Tatranka
Fidorka
Mars
function VendingMachine() {
const items = [
'Tatranka',
'Fidorka',
'Mars'
];
return ...
}
function VendingItem() {
return <button />{name}
}1x
1x
1x
import React, { useState } from 'react';
function Counter() {
const [name, setName] = useState('nobody');
function handleGiveName(name: string) {
setName(name);
}
return <div>
My name is {name}.
<button onClick={() => handleGiveName('Martin')}>
Give me name
</button>
</div>
}initial value
Tatranka
Fidorka
Mars
3x
5x
0x
type Props = {
name: string;
}
function Item({name} : Props) {
...
}import React from 'react';
type State = {
counter: number;
}
export class MyComponent extends React.Component<{}, State> {
state = {
counter: 0
};
increment() {
this.setState({ counter: this.state.counter + 1 });
}
render() {
const { counter } = this.state;
return <div>
Counter: {counter}
<button type="button" onClick={() => this.increment()}>Increment</button>
</div>
}
}type Props = {
pregeneratedCount: number
}
type State = {
generatedNumbers: number[];
}
export class NumberGeneratorClass extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const generatedNumbers = [...Array(props.pregeneratedCount)].map(() => Math.random());
this.state = {
generatedNumbers
};
}
generateNew() {
this.setState({ generatedNumbers: [...this.state.generatedNumbers, Math.random()] });
}
render() {
const { generatedNumbers } = this.state;
return <div>
{generatedNumbers.map((num, index) => <div key={index}>{num}</div>)}
<button type="button" onClick={() => this.generateNew()}>Generate new</button>
</div>
}
}function MyComponent() {
const random = Math.random();
if (random < 0.5) {
return <span>lower</span>
} else {
return <span>higher</span>
}
}function MyComponent() {
const random = Math.random();
return <span>
{random < 0.5 ? 'lower' : 'higher'}
</span>
}function MyComponent() {
const condition = true;
return <>
{condition && <span>It's true</span>}
</>
}Tatranka
Fidorka
Mars
3x
5x
0x
out of stock
<div style={{color: 'red'}}>Out of stock</div>Component
User info
ArticleList
Article
Today Weather
Article
I am smart, I know what to do 💡
❤️ "User liked an article"
type Props = {
article: Article;
onLike: () => void;
};
function Article(props: Props) {
return <>
<h1>{article.title}</h1>
<p>{article.shortText}</p>
<button onClick={props.onLike}>❤️ Like</button>;
</>
}<Article article={article} onLike={() => handleLike(article)} />parent component:
child component:
Tatranka
Fidorka
Mars
3x
5x
0x
out of stock
Price: 20,-
debugger;import './App.css';
function Component() {
return <div className="red">Hello</div>
}.red {
color: red;
}App.css
App.tsx
import styles from './Component.module.css';
function Component() {
return <div className={styles.red}>Hello</div>
}.red {
color: red;
}App.module.css
App.tsx
import cn from 'classnames';
function ValidationState() {
const [invalid, setInvalid] = useState(false);
return <div className={cn({ red: invalid })}>
Status
</div>
}import cn from 'classnames';
import styles from './ValidationState.module.css';
function ValidationState() {
const [invalid, setInvalid] = useState(false);
return <div className={cn({ [styles.red]: invalid })}>
Status
</div>
}Tatranka
Fidorka
Mars
3x
5x
0x
out of stock
Price: 20,-
export const MyMouse = () => {
const [mousePosition, setMousePosition] = useState({x: 0, y: 0});
useEffect(() => {
const onMouseMove = (event: MouseEvent) => {
setMousePosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', onMouseMove);
return () => {
window.removeEventListener('mousemove', onMouseMove);
};
}, []);
const {x, y} = mousePosition;
return (
<div>My mouse x position is {x} and y position is {y}</div>
);
};
<MyBytton onClick={...}>
<Icon> Click me!
</MyBytton>function MyButton(props) {
return (
<button className="blue-button" onClick={props.onClick}>
{props.children}
</button>
)
}<DropdownComponent label="Open dropdown">
Hello, this is dropdown!
</DropdownComponent>Tatranka
Fidorka
Mars
🪙
Insert coin
3x
5x
0x
out of stock
+10 coins
+20 coins
+50 coins
Coins: 50,-
Price: 20,-
<DropdownComponent label="Insert coins">
<button>+ 10 coins</button>
<button>+ 20 coins</button>
<button>+ 30 coins</button>
</DropdownComponent>function Component() {
const [inputName, setInputName] = useState(name);
return <>
{inputName}
<input
value={inputName}
onChange={(e) => setInputName(e.target.value)} />
</>
}Tatranka
Fidorka
Mars
🪙
Insert coin
3x
5x
0x
out of stock
+10 coins
+20 coins
+50 coins
Any amount
Coins: 50,-
Price: 20,-
+10 coins
+20 coins
+50 coins
Any amount
Tatranka
Fidorka
Mars
Price: 20,-
Coins: 50,-
🪙
Insert coin
3x
5x
0x
out of stock
import axios from 'axios';
const response = await axios.get('/api/users');
console.log(response);
const response = await fetch('/api/users')
const json = await response.json();import axios from 'axios';
const payload = { name: 'Martin' };
const response = await axios.post('/api/users', payload);
console.log(response);
import axios from 'axios';
const payload = { name: 'Martin' };
axios.post('/api/users', payload)
.then(response => console.log(response));
type Response = {
id: number;
name: string;
age: number;
}
axios.get<Response>('/api/users/1')GET https://api.chucknorris.io/jokes/randomconst useMouseMove = () => {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const onMouseMove = (event: MouseEvent) => {
setMousePosition({
x: event.clientX,
y: event.clientY,
});
};
window.addEventListener('mousemove', onMouseMove);
return () => {
window.removeEventListener('mousemove', onMouseMove);
};
}, []);
return { x: mousePosition.x, y: mousePosition.y };
};type ContextValue = boolean;
const MyContext = React.createContext<ContextValue>(false);
function App() {
return <MyContext.Provider value={true}>
<Component />
</MyContext.Provider>;
}
function Component() {
const value = useContext(MyContext);
return <div>{value}</div>;
}type ContextValue = {
value: number;
setValue: (value: ContextValue['value']) => void
};
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
function App() {
const [value, setValue) = useState(0);
return <MyContext.Provider value={{ value, setValue}}>
<Component />
</MyContext.Provider>;
}
function Component() {
const {value, setValue} = useContext(MyContext);
return <>
<div>{value}</div>
<button onClick={() => setValue(value + 1)}>Click</button>
</>;
}VITE_API_URL=https://simple-vending-api-demo.onrender.com npm run devexport const API_URL = import.meta.env.VITE_API_URL
|| 'http://localhost:3000';login: admin
password: passwd1
| 1x | Tatranka | 30,- |
| 5x | Fidorka | 20,- |
| 0x | Mars | 30,- |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /login | Returns JWT token | No |
| GET | /machines | List all machines | Yes |
| GET | /machines/:id | Get specific machine | Yes |
| PUT | /machines/:id/items | Update items array | Yes |
const res = await fetch(`${API_URL}/login`, {
method: 'POST',
body: JSON.stringify({ username, password }),
credentials: 'include' // Important for HttpOnly cookies
});type UserContext = {
user: User | null,
token: string | null;
login: (user: User) => void;
logout: () => void;
}function Component() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current.focus();
}
return <div>
<input ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</div>
}npm install formik yupimport { Formik, Field, ErrorMessage } from 'formik';
import * as yup from 'yup';
const schema = yup.object().shape({
email: yup.string().required().email(),
age: yup.number().required().positive().integer()
})
const initialValues = {
email: '',
age: 0
}
export function MyForm() {
return (
<Formik
initialValues={initialValues}
validationSchema={schema}
onSubmit={values => console.log(values)}
>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field type="email" name="email" />
<ErrorMessage name="email" component="div" />
<Field type="number" className="error" name="age" />
<ErrorMessage name="age" className="error" component="div"/>
<button type="submit">
Submit
</button>
</form>
)}
</Formik>
);
}What should we know from basics?
GET https://api.chucknorris.io/jokes/random// create a new project
npx create-react-app react-playground --template typescript
// install axios
npm i axios
npm startimport { useRef } from 'react';
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return <div>
<input ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</div>
}Parent
Child
do something
Parent
Child
do something
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => inputRef.current?.focus();
return <>
<Child ref={inputRef} />
<button onClick={handleClick}>
Focus
</button>
</>
}
const Child = forwardRef<HTMLInputElement>((props, ref) => {
return <div>
<input ref={ref} />
</div>
})function Parent() {
const childApiRef = useRef<{ focus: () => void }>(null);
const handleClick = () => childApiRef.current?.focus();
return <>
<Child ref={childApiRef} />
<button onClick={handleClick}>
Focus
</button>
</>
}
const Child = forwardRef<{ focus: () => void }>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}), [inputRef])
return <div>
<input ref={inputRef} />
</div>
})import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
export type Album = {
id: number;
title: string;
rating: number;
}
export const AlbumVoting = () => {
const [albums, setAlbums] = useState(new Array(15).fill({
id: 0,
title: '',
rating: 0
}).map((album, index) => ({ ...album, id: index })));
function handleChange (updatedAlbum: Album) {
setAlbums(albums => albums.map(album => album.id === updatedAlbum.id ? updatedAlbum : album));
};
return <div>
{albums.map(album => <AlbumItem key={album.id} album={album} onChange={handleChange} />)}
</div>
}
import { Album } from "./AlbumVoting";
type Props = {
album: Album,
onChange: (updated: Album) => void
}
export const AlbumItem = ({ album, onChange }: Props) => {
function handleChange(change: Partial<Album>) {
onChange({
...album,
...change
});
}
return <p>
Title:
<input value={album.title}
onChange={(e) => handleChange({ title: e.target.value })}
/>
Rating
<input type='number'
value={album.rating}
onChange={(e) => handleChange({ rating: Number(e.target.value) })}
/>
</p>
}AlbumVoting.tsx
AlbumItem.tsx
const ExpensiveComponentMemoized = React.memo(function ExpensiveComponent() {
...
});
<ExpensiveComponentMemoized />const useFibonacci = (n) => {
const result = useMemo(() => fibonacci(n), [n]);
return result;
}
function fibonacci(n) {
return n < 1 ? 0
: n <= 2 ? 1
: fibonacci(n - 1) + fibonacci(n - 2)
}type Props = {
me: string;
}
function Component({me}: Props) {
const handleClick = useCallback(
(name) => console.log(`Hello ${name} and ${me}`)
, [me]);
return <ExpensiveComponent onClick={handleClick} />;
}import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
export type Album = {
id: number;
title: string;
rating: number;
}
export const AlbumVoting = () => {
const [albums, setAlbums] = useState(new Array(15).fill({
id: 0,
title: '',
rating: 0
}).map((album, index) => ({ ...album, id: index })));
function handleChange (updatedAlbum: Album) {
setAlbums(albums => albums.map(album => album.id === updatedAlbum.id ? updatedAlbum : album));
};
return <div>
{albums.map(album => <AlbumItem key={album.id} album={album} onChange={handleChange} />)}
</div>
}
import { Album } from "./AlbumVoting";
type Props = {
album: Album,
onChange: (updated: Album) => void
}
export const AlbumItem = ({ album, onChange }: Props) => {
function handleChange(change: Partial<Album>) {
onChange({
...album,
...change
});
}
return <p>
Title:
<input value={album.title}
onChange={(e) => handleChange({ title: e.target.value })}
/>
Rating
<input type='number'
value={album.rating}
onChange={(e) => handleChange({ rating: Number(e.target.value) })}
/>
</p>
}AlbumVoting.tsx
AlbumItem.tsx
type ContextValue = boolean;
const MyContext = React.createContext<ContextValue>(false);
function App() {
return <MyContext.Provider value={true}>
<Component />
</MyContext.Provider>;
}
function Component() {
const value = useContext(MyContext);
return <div>{value}</div>;
}type ContextValue = {
value: number;
setValue: (value: ContextValue['value']) => void
};
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
function App() {
const [value, setValue) = useState(0);
return <MyContext.Provider value={{ value, setValue}}>
<Component />
</MyContext.Provider>;
}
function Component() {
const {value, setValue} = useContext(MyContext);
return <>
<div>{value}</div>
<button onClick={() => setValue(value + 1)}>Click</button>
</>;
}type ContextValue = {
value: number;
setValue: (value: ContextValue['value']) => void
};
// not exported - private
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
export function MyContextProvider({children}: PropsWithChildren) {
const [value, setValue) = useState(0);
return <MyContext.Provider value={{ value, setValue}}>
{children}
</MyContext.Provider>;
}
// only way to read the value:
export const useMyContext = () => useContext(MyContext);
// in another file
import {useMyContext} from './my-context';
function Component() {
const { value, setValue } = useMyContext();
return <>
<div>{value}</div>
<button onClick={() => setValue(value + 1)}>Click</button>
</>;
}
<MyContextProvider>
<Component />
</MyContextProvider>type ContextValue = {
value: number;
changeValue: (newValue: ContextValue['value']) => void;
}
const MyContext = React.createContext<ContextValue>({} as unknown as ContextValue);
type Props = {
initialState: ContextValue['value'];
children: React.ReactNode
}
export function MyContextProvider({initialState, children}: Props) {
const [value, setValue] = useState(initialState);
return <MyContext.Provider value={{value, setValue}}>
{children}
</MyContext.Provider>;
}
<MyContextProvider initialState={5}>
...
</MyContextProvider>export function UserInfoPane() {
const { user } = useUser();
return <div>
{user ? <CurrentUserInfo /> : <LoginForm />}
</div>
}export function LoginForm() {
const { login } = useUser();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
return <div>
<input placeholder="username" value={username} onChange={(e) => setUsername(e.target.value)} />
<input placeholder="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={() => login({username, email})}>Login</button>
</div>
}
const useUser = () => useContext(UserContext);export function UserInfoPane() {
const { user } = useUser();
return <div>
{user ? <CurrentUserInfo /> : <LoginForm />}
</div>
}UserContextProvider
UserInfoPane
CurrentUserInfo
LoginForm
= missing
= refactor
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})import {add} from './add';
test('adding two numbers', () => {
expect(add(1, 3)).toBe(4);
})import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})import {add} from './add';
it('adds numbers', () => {
// Arrange
const a = 1;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(4);
})expect(add(1, 3)).toBe(4);subject
matcher
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).not.toBe(5);
})expect('How are you?').toEqual(expect.stringContaining('How'));expect({a: 1, b: 2}).toBe({a: 1, b: 2}); // ❌ DON'T
expect({a: 1, b: 2}).toEqual({a: 1, b: 2}); // ✅ DO
expect({a: 1, b: 2}).toEqual(expect.objectContaining({a: 1}));
expect({a: 1, b: 2}).toHaveProperty('a', 1);
expect({
one: 1,
two: {
nested: 2
}
}).toHaveProperty('two.nested', 2);expect([1,2,3]).toBe([1,2,3]); // ❌
expect([1,2,3]).toEqual([1,2,3]); // ✅
expect([1,2,3]).toEqual(expect.arrayContaining([1,2]));expect(testedFn()).toThrow(); // ❌ DON'T
expect(() => testedFn()).toThrow(); // ✅ DOfunction doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}
// ❌ DON'T - better to use mocks (later)
it('calls the function n times', () => {
expect.assertions(3);
function fn() {
expect(true).toBe(true);
}
doNTimes(fn, 3)
}){
"name": "...",
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!(axios))"
]
},
...
}test.skip('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
npm testnpm install vitestexport function divideWithRemainder(a: number, b: number) {
if (b === 0) {
throw new Error('Cannot divide by zero.');
}
const result = Math.ceil(a / b);
const remainder = a % b;
return {
result,
remainder
};
}
const fn = vitest.fn(); // empty function
// pass implementation
const fn = vitest.fn(() => {
return 5;
});
// override implementation
fn.mockImplementation(() => {
return 6;
});
fn.mockReturnValue(5);
fn.mockResolvedValue(5); // returns promise which resolves in 5
fn.mockReturnValueOnce(1);
fn.mockReturnValueOnce(2);
fn.mockReturnValue(3);
fn() // 1
fn() // 2
fn() // 3
fn() // 3const fn = vitest.fn(() => 42); // empty function
expect(fn).toHaveBeenCalled(); // fails
fn();
expect(fn).toHaveBeenCalled(); // passes
expect(fn).toHaveBeenCalledTimes(1);
fn(1,2,3)
expect(fn).toHaveBeenLastCalledWith(1,2,3);
expect(fn).toHaveReturnedWith(42); // test outputfunction doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}vitest.spyOn(Math, 'random');
Math.random();
expect(Math.random).toHaveBeenCalled();
// change behavior
const randomMock = vitest.spyOn(Math, 'random');
randomMock.mockReturnValue(1);
expect(Math.random()).toBe(1);import { vitest, type Mocked} from 'vitest';
import { fetchUser } from './fetch-user';
import axios from 'axios';
vitest.mock('axios');
const mockedAxios = axios as Mocked<typeof axios>;
it('fetches user data', async () => {
const response = { data: { id: 1, name: 'Martin' } };
mockedAxios.get.mockResolvedValue(response);
expect(await fetchUser()).toEqual({ id: 1, name: 'Martin' });
});
import axios from 'axios';
function fetchUser() {
return axios.get('/url').then((response) => response.data);
}
| Type of Query | 0 Matches | 1 Match | >1 Matches | Retry (Async/Await) |
|---|---|---|---|---|
| Single Element | ||||
| getBy... | Throw error | Return element | Throw error | No |
| queryBy... | Return null | Return element | Throw error | No |
| findBy... | Throw error | Return element | Throw error | Yes |
| Multiple Elements | ||||
| getAllBy... | Throw error | Return array | Return array | No |
| queryAllBy... | Return [ ] | Return array | Return array | No |
| findAllBy... | Throw error | Return array | Return array | Yes |
import { screen } from '@testing-library/react';
screen.getByRole('button', {name: /submit/i});getByRole('button', { name: 'Submit' });
// // checked input type checkbox
getByRole('checkbox', { checked: true });
// usually h2
getByRole('heading', { level: 2 });screen.getByText('Hello World'); // full string match
screen.getByText('llo Worl', {exact: false}); // substring match
screen.getByText('hello world', {exact: false}); // ignore case
screen.getByText(/World/); // regexp
// custom function
screen.getByText((content, element) => content.startsWith('Hello'));<div class="background red" data-testid="background">
...
</div>
const backgroundEl = screen.getByTestId('background');import { screen, within } from '@testing-library/react';
const container = screen.getByTestId('container');
const helloMessage = within(container).getByText('hello');waitForElementToBeRemoved)import { screen, waitFor } from '@testing-library/react';
await waitFor(() => {
expect(screen.getByText('Some text')).toBeInTheDocument();
});// click on an element
fireEvent.click(screen.getByText('Login'));
// change input value
fireEvent.change(getByLabelText(/username/i), {target: {value: 'martin'}});
// check text content
const element = screen.getByTestId('title');
expect(element).toHaveTextContent('This is title');
// check if is in the document
const element = screen.queryByText('Submit');
expect(element).toBeInTheDocument();
// test focus
const input = screen.getByTestId('password');
expect(input).toHaveFocus();
// test checkbox state
const rememberPass = screen.getByRole('checkbox', {name: 'Remember password'});
expect(rememberPass).toBeChecked();import { render } from '@testing-library/react';
it('renders', () => {
render(<App />);
const element = screen.queryByText('Hello');
expect(element).toBeInTheDocument();
});
screen.debug()screen.logTestingPlaygroundURL()import { screen } from '@testing-library/react';1️⃣ install
npm install -D @testing-library/react @testing-library/jest-dom jsdom2️⃣ configure vite.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js'
}
})
3️⃣ src/setupTests.ts
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
// jsdom cleanup
afterEach(() => {
cleanup();
});4️⃣ tsconfig.app.json
"scripts": {
"test": "vitest"
}5️⃣ npm script
{
"compilerOptions": {
"types": [
"vite/client",
"vitest/globals",
"@testing-library/jest-dom"
],
...
},
...
}import { act, renderHook } from "@testing-library/react";
import { useCounter } from "./use-counter";
it('increments', () => {
const {result} = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
})export function useCounter() {
const [count, setCount] = useState(0);
const increment =() => setCount(count + 1);
return { count, increment };
}export function useCounter(initialValue: number) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const randomize = () => setCount(Math.random());
return { count, increment, randomize };
}
Children
<WrapperComponent>
</WrapperComponent>
function Component() {
return <MousePosition render={
(x, y) => <p>Position is x={x}, y={y}</p>
} />
}
type Props = {
render: (x: number, y: number) => React.ReactNode;
}
function MousePosition({ render }: Props) {
...
return <>{render(position.x, position.y)}</>;
}export function MyComponent() {
function setWallet(n: number) { /* ... */ }
return (
<DropdownButton label="Menu">
<button onClick={() => setWallet(10)}>+10</button>
<button onClick={() => setWallet(30)}>+30</button>
<button onClick={() => setWallet(50)}>+50</button>
</DropdownButton>
);
}export function DropdownButton({ label, children }: PropsWithChildren<Props>) {
const [isOpen, setIsOpen] = useState(false);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setIsOpen(!isOpen)}>{label}</button>
{isOpen && (
<div
style={{
position: "absolute",
background: "white",
padding: "10px",
border: "1px solid black",
}}
>
{children}
</div>
)}
</div>
);
}
export function MyComponent() {
function setWallet(n: number) { /* ... */ }
return (
<DropdownButton label="Menu">
{({close}) => <>
<button onClick={() => setWallet(10)}>+10</button>
<button onClick={() => setWallet(30)}>+30</button>
<button onClick={() => setWallet(50)}>+50</button>
<button onClick={close}>X</button>
</>}
</DropdownButton>
);
}type DropdownApi = {
close: () => void;
};
type Props = {
label: string;
children: (api: DropdownApi) => React.ReactNode;
};
export function DropdownButton({ label, children }: Props) {
const [isOpen, setIsOpen] = useState(false);
function close() {
setIsOpen(false);
}
return (
<div style={{ position: "relative" }}>
<button onClick={() => setIsOpen(!isOpen)}>{label}</button>
{isOpen && (
<div
style={{
position: "absolute",
background: "white",
padding: "10px",
border: "1px solid black",
}}
>
{children({ close })}
</div>
)}
</div>
);
}
<ErrorBoundary>
<Joke />
</ErrorBoundary>import React, { PropsWithChildren } from "react";
type Props = PropsWithChildren;
type State = {
hasError: boolean;
error?: unknown;
}
export class MyErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: unknown) {
return { hasError: true, error };
}
// optional
componentDidCatch(error: unknown, errorInfo: ErrorInfo) {
console.error(error, errorInfo);
}
render() {
const {error, hasError} = this.state;
if (hasError) {
// fallback
return <div>{String(error)}</div>;
}
return this.props.children;
}
}export function ThrowComponent() {
if (Math.random() < 0.5) {
throw 'Something went wrong.'
};
return <p>Didn't throw</p>
}Routable pages
Navbar
import { Outlet } from "react-router";
export function Layout() {
return <div>
<NavBar />
<Outlet />
</div>
}Here other routes render
Routable pages
<Outlet />
<Navbar />
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
... // (later)
]);
function App() {
return (
<RouterProvider router={router} />
);
}const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />
},
{
path: 'about',
element: <About />
},
{
path: 'articles',
element: <ArticlesContainer />,
children: [
{
path: ':articleId',
element: <Article />
}
{
path: ':articleId/comments',
element: <ArticleComments />
}
]
}
]
}
]);Must have <Outlet /> inside
Layout component
Routable pages
Navbar
<Outlet>
Routable pages
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/articles">Articles</Link>
<Link to="/articles/{articleId}">Specific article</Link>const navigate = useNavigate();
navigate('/articles');import { useParams } from "react-router-dom";
function Joke() {
const params = useParams();
return (
{params.category}
);
}/path-segment/:pathParam/something?query1=one&query2=twouseSearchParams
useParams
// joke.tsx
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
type JokeResponse = {
value: string;
};
export function Joke() {
const [joke, setJoke] = useState<string | null>(null);
const { category } = useParams();
useEffect(() => {
const queryParams = new URLSearchParams();
if (category) {
queryParams.set('category', category);
}
axios<JokeResponse>(
`https://api.chucknorris.io/jokes/random?${queryParams.toString()}`
).then((response) => setJoke(response.data.value));
}, [category]);
return <p>{joke}</p>;
}// joke-categories.tsx
import axios from "axios";
import { useEffect, useState } from "react";
export function JokeCategories() {
const [categories, setCategories] = useState<string[]>([]);
useEffect(() => {
axios<string[]>("https://api.chucknorris.io/jokes/categories").then(
(response) => setCategories(response.data)
);
}, []);
return <>
{/* TODO */}
</>;
}// navbar.tsx
import { Link } from "react-router-dom"
export const NavBar = () => {
return <ul>
<Link to="/">Home</Link>
<Link to="/categories">Joke categories</Link>
</ul>
}stale-while-revalidate
import useSWR from 'swr'
const fetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then(res => res.json());
function Joke() {
const { data, isValidating } = useSWR(
`https://api.chucknorris.io/jokes/random`,
fetcher
);
return (
<p>{isValidating ? 'Loading...' : data.value}</p>
);
}import useSWR, {useSWRConfig} from 'swr'
const fetcher = (...args) =>
fetch(...args).then(res => res.json());
function Joke() {
const { data, isValidating } = useSWR(
`https://api.chucknorris.io/jokes/random`,
fetcher
);
const { mutate } = useSWRConfig();
return <div>
<button onClick={() => mutate(`https://api.chucknorris.io/jokes/random`)}>
Load next
</button>
<p>{isValidating ? 'Loading...' : data.value}</p>
</div>;
}function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ComponentWithLoading />
</Suspense>
)
}const { data, isValidating } = useSWR(url, fetcher, { suspense: true });const { data, isValidating } = useSWR(url, fetcher);import { SWRConfig } from 'swr';
import { useNavigate } from 'react-router-dom';
import { fetcher } from './fetcher';
function App() {
const navigate = useNavigate();
return (
<SWRConfig
fetcher,
value={{
onError: (error, key) => {
if (error.status === 401 || error.status === 403) {
// 1. Global redirect logic
// 2. Clear token if using localStorage
// 3. Show toast notification if desired
console.log('Session expired, redirecting...');
navigate('/login');
}
}
}}
>
{/* All children now inherit this error handling */}
</SWRConfig>
);
}export const fetcher = async <T,>(...args: Parameters<typeof fetch>): Promise<T> => {
const res = await fetch(...args);
// If the status is not 200-299
if (!res.ok) {
const error = new Error('An error occurred while fetching the data.');
// Attach extra info to the error object.
// We cast to 'any' or a custom interface so TS doesn't complain about 'status'
(error as any).status = res.status;
(error as any).info = await res.json();
throw error;
}
return res.json();
};<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/about" element={<AboutPage />} />
{/* Protected routes */}
<Route path="/dashboard" element={
<RequireAuth>
<DashboardPage />
</RequireAuth>
} />
</Routes>
</BrowserRouter>function RequireAuth({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}// default export
const ComponentLazy = React.lazy(
() => import('./ComponentDefaultExport')
);
// rename named export as default
const ComponentLazy = React.lazy(() =>
import('./ComponentNamedExport')
.then(({ComponentNamedExport}) => ({default: ComponentNamedExport}))
);
function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ComponentLazy prop="value" />
</Suspense>
);
}const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [ {
path: 'about',
// key must be "Component"
lazy: () =>
import("./about").then(({ About }) => ({Component: About}))
},
]
}
]);Must be under Component key
import {createPortal} from 'react-dom';
function Component() {
return <div>
{createPortal(<div>Hello</div>, document.body)}
</div>
}<Modal>
<h1>Hello</h1>
<p>I am modal</p>
</Modal>type Props = {
children: React.ReactNode[];
}
type ExpandedState = Record<number, boolean>;
export function Accordeon({ children }: Props) {
const [expandedState, setExpandedState] = useState<ExpandedState>({});
const toggleExpansionHandler = useCallback(
(index: number) => {
setExpandedState(expandedState => {
if (expandedState[index]) {
delete expandedState[index];
} else {
expandedState[index] = true;
}
return {
...expandedState,
};
});
},
[],
);
return <>
{React.Children.map(children, (child, index) => (
<div>
<button onClick={() => toggleExpansionHandler(index)}>
{expandedState[index] ? '➖' : '➕'}
</button>
{child && expandedState[index] && child}
</div>
))}
</>;
}
type State = number;
type Increment = { type: 'increment'; payload: number };
type Randomize = { type: 'randomize' };
type Action = Increment | Randomize;
function reducer(state: State, action: Action) {
switch (action.type) {
case "increment":
return state + 1;
case "randomize":
return Math.random();
default:
return state;
}
}
export function Counter() {
const [ state, dispatch ] = useReducer(reducer, 0);
return <div>
Counter: {state}
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>Increment</button>
<button onClick={() => dispatch({ type: 'randomize' })}>Randomize</button>
</div>
}Initial state
type State = {
user: User | null;
timeLeft: number
}
type Action = LOGIN | LOGOUT | SESSION_TICK;{
jokeSlice: {
currentJoke: "Chuck Norris can speak Braille.",
isLoading: false
},
counterSlice: {
count: 5
},
uiSlice: {
dropdownVisible: false
}
}View
Reducer
Store
dispatch an action
update the store
view reads data from the store
increment = (state) => {
state.count += 1;
}// /src/store/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
export type CounterState = {
count: number;
};
const initialState: CounterState = {
count: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state, action) => {
state.count += 1; // Immer
}
},
});
export const { increment } = counterSlice.actions;
export const counterReducer = counterSlice.reducer;
// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { jokeReducer } from './jokeSlice';
import { counterReducer } from './counterSlice';
import { uiReducer } from './uiSlice';
export const store = configureStore({
reducer: {
jokeSlice: jokeReducer,
counterSlice: counterReducer,
uiSlice: uiReducer
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// index.tsx
import { store } from './store/store'
import { Provider } from 'react-redux'
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import { useAppSelector } from '../store/hooks';
function Counter() {
const count = useAppSelector(state => state.counterSlice.count);
return <div>Current count: {count}</div>
}import { useAppDispatch } from '../store/hooks';
import { increment } from "../store/counterSlice";
function IncrementButton() {
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}// src/store/jokeSlice.js
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchJoke = createAsyncThunk('Fetch joke',
async (category, { getState }) => {
const { loading } = getState().jokeSlice;
if (loading !== 'pending') {
return
}
const response = await fetch(`https://api.chucknorris.io/jokes/random?category=${category}`)
const data = await response.json();
return data.value
}
)// src/store/jokeSlice.js
export const jokeSlice = createSlice({
// ...
extraReducers: (builder =>
builder
.addCase(fetchJoke.pending, (state) => {
if (state.loading === 'idle') {
state.loading = 'pending'
}
})
.addCase(fetchJoke.fulfilled, (state, action: PayloadAction<string>) => {
if (state.loading === 'pending') {
state.loading = 'idle';
state.joke = action.payload;
}
})
)
});
extraReducers = for dealing with actions defined somewhere else
const dispatch = useDispatch();
dispatch(fetchJoke(category));function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);useEffect(() => {
const id = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(id);
}, []);function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
Hello {fullName}
</div>
}function ChatInput() {
return <Button useData={useDataWithLogging} />
}expect(displayName === name).toBe(true);const Parent = () => {
const SecondChild = () => {
return <div> <SecondChild/> </div>
}
const Child = () => {
return <div> <SecondChild/> </div>
}
return (
<div>
<Child/>
</div>
)
}// album-item.tsx
import type { Album } from "./album-voting";
...// album-voting.tsx
import { AlbumItem } from "./album-item";
...function MyInput({ ref }: Props) {
return <input ref={ref} />;
}
<MyInput ref={ref} />;const UserContext = React.createContext({});
function UserContextProvider({ children }) {
return (
<UserContext value={...}>
{children}
</UserContext>
);
}import {Expandable} from './Expandable';
async function Notes() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<ClientComponent key={note.id}>
<p note={note} />
</ClientComponent>
))}
</div>
)
}// Server component
function EmptyNote () {
async function createNoteAction() {
// Server Function
'use server';
await db.notes.create();
}
return <Button onClick={createNoteAction}/>;
}"use server";
export async function createNoteAction() {
await db.notes.create();
}"use client";
import {createNote} from './actions';
function EmptyNote() {
return <Button onClick={createNote}/>;
}function Joke() {
const response = use(axios('https://api.chucknorris.io/random'));
const joke = response.data.value;
return <p>{joke}</p>
}
function App() {
return <Suspense fallback="Loading">
<Joke />
</Suspense>
}function List() {
const [state, setState] = useState([1, 2, 3]);
const [optimisticState, addOptimistic] = useOptimistic(
state,
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
return [...currentState, optimisticValue]
}
);
function handleAddNew(n: number) {
addOptimistic(n);
axios.post('/new-value').then((stateFromServer) => setState(stateFromServer));
}
return <ul>
{optimisticState.map(value => <li>{value}</li>)}
<button onClick={() => handleAddNew(Math.random())}>add new</button>
</ul>
}npm init -y
npm i react react-dom
npm i -D typescript \
webpack webpack-cli webpack-dev-server \
ts-loader @types/react @types/react-dom \
html-webpack-plugin
npx tsc --init
// Update tsconfig:
"jsx": "react"
"outDir": "./dist",
webpack.config.js
npx webpack serve // dev server
npx webpack // buildconst path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
output: {
filename: 'main.bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({template: './public/index.html'}),
],
};
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.optimize.SplitChunksPlugin({minSize: 1}),
new HtmlWebpackPlugin({template: './public/index.html'}),
]
};