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@latest
Project 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
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
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>
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
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>
);
};
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,-
<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 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/random
const 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>
</>;
}
Tatranka
Fidorka
Mars
Price: 20,-
Coins: 50,-
🪙
Insert coin
3x
5x
0x
out of stock
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>
}
import {it} from 'vitest';
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})
import {test} from 'vitest';
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(); // ✅ DO
function 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 i -D @babel/plugin-proposal-private-property-in-object
npm test
"scripts": {
"test": "vitest"
}
npm install -D vitest
export 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 = jest.fn(); // empty function
// pass implementation
const fn = jest.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() // 3
const fn = jest.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 output
function doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}
import { expect, vi } from 'vitest'
vi.spyOn(Math, 'random');
Math.random();
expect(Math.random).toHaveBeenCalled();
// change behavior
const randomMock = jest.spyOn(Math, 'random');
randomMock.mockReturnValue(1);
expect(Math.random()).toBe(1);
import {it, expect, vi} from 'vitest';
import { fetchUser } from './fetch-user';
import axios from 'axios';
vi.mock('axios');
const mockedAxios = vi.mocked(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);
}
npm install --D @testing-library/react @testing-library/dom
npm install --D jsdom @testing-library/jest-dom
// vite.config.js
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ['./vitest-setup.js'],
}
})
// vitest-setup.js
import '@testing-library/jest-dom/vitest'
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. render -> check LoginForm is visible
{
"name": "...",
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!(axios))"
]
},
...
}
package.json
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 };
}
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=two
useSearchParams
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>
}
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;
}
},
});
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>
);
}
npm install formik yup
import { 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>
);
}