React Advanced
🙋♂️
Frontend engineer
- 10 years FE experience
- AngularJS, Angular, Ember, React
- currently Staff engineer at Productboard
❤️ 📸
❤️ 🛞
Slides + code
Schedule
- 9-12 morning
- recap of the previous day
- breaks: 10:00, 11:00
- 12-13 lunch
- 13-17 afternoon
- breaks: 14:00, 15:00, 16:00
Course
- exercises build on top of each other!
- discussions welcomed
Your experience?
Setup IDE
Install
- Google Chrome
- nodejs
- Visual Studio Code
- eslint
- VS Code ➡ extensions ➡ search for eslint
- React dev tools
Create a new project
- create-react-app = tool for scaffolding react app
npx create-react-app react-playground
🧠 React basics
- components, component tree
- props, events
- styling the app
- children props
- controlled input
- useState, useEffect
- making HTTP requests
- custom hooks
- React Context
➡️ Let's make http request
- open API request in browser to see structure of response
- display joke in the component
- create a button to load another joke
- disable button when loading the joke
GET https://api.chucknorris.io/jokes/random
// install axios
npm i axios
npm start
Let's do more React! 💪
React hooks
- ✅ useState
- ✅ useEffect
- ✅ useContext
- useRef
- useImperativeHandle
- useCallback
- useMemo
- useReducer
useRef
useRef
- manipulate with DOM elements
- object with mutable current property
import { useRef } from 'react';
function Component() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current?.focus();
}
return <div>
<input ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</div>
}
Child to parent?
Parent
Child
do something
Control child component?
Parent
Child
do something
Expose element from
a component
- ref cannot be used as prop
- need to wrap component with forwardRef
Parent
Child
ref
<button>
Expose element from
a component
function Parent() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current?.focus();
return <>
<MySuperInput ref={inputRef} />
<button onClick={focusInput}>
Focus
</button>
</>
}
const MySuperInput = forwardRef((props, ref) => {
return <div>
<input ref={ref} />
</div>
})
Expose imperative API
- used to expose API of Child component to the Parent
function Parent() {
const inputApiRef = useRef(null);
const focusInput = () => inputApiRef.current?.focus();
return <>
<MySuperInput ref={inputApiRef} />
<button onClick={focusInput}>
Focus
</button>
</>
}
const MySuperInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}), [inputRef])
return <div>
<input ref={inputRef} />
</div>
})
React hooks
- ✅ useState
- ✅ useEffect
- ✅ useContext
- ✅ useRef
- ✅ useImperativeHandle
- useCallback
- useMemo
- useReducer
Performance
Problem
- lot of rerenders
- every render creates new function, object etc
- DOM operations are expensive
React Virtual DOM
- virtual representation of DOM = big object
- React updates the virtual DOM
- then creates a diff agains the real DOM
- and applies only necessary changes to the real DOM
Profiler in dev tools
import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
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) {
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";
export const AlbumItem = ({ album, onChange }) => {
function handleChange(change) {
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.jsx
AlbumItem.jsx
- shows rerenders
- shows why




React.memo
- rerenders component only on prop change
const ExpensiveComponentMemoized = React.memo(function ExpensiveComponent() {
...
});
<ExpensiveComponentMemoized />
useMemo
- precompute value
- for computation-expensive values
- avoids main thread lock
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)
}
useCallback
- used to retain a single function reference
- avoids problem with recreating handler every render
function Component({me}) {
const handleClick = useCallback(
(name) => console.log(`Hello ${name} and ${me}`)
, [me]);
return <ExpensiveComponent onClick={handleClick} />;
}
➡️ Optimize album voting
- only row which is updated should rerender
export const AlbumItem = ({ album, onChange }) => {
function handleChange(change) {
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>
}
import { useState } from "react";
import { AlbumItem } from "./AlbumItem";
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) {
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>
}
AlbumItem.jsx
AlbumVoting.jsx
React hooks
- ✅ useState
- ✅ useEffect
- ✅ useContext
- ✅ useRef
- ✅ useImperativeHandle
- ✅ useCallback
- ✅ useMemo
- useReducer
React Context
Context
- "global" state for subtree of components
- avoids passing props deep
- Provider + Consumer
const MyContext = React.createContext(false);
function App() {
return <MyContext.Provider value={true}>
<Component />
</MyContext.Provider>;
}
function Component() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
Context
- pass object with value + setter
const MyContext = React.createContext({});
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>
</>;
}
Encapsulate context
- Provider component instead of App component
// not exported - private
const MyContext = React.createContext({});
export function MyContextProvider({children}) {
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>
Context initial state
- pass via props
const MyContext = React.createContext({});
export function MyContextProvider({initialState, children}: Props) {
const [value, setValue] = useState(initialState);
return <MyContext.Provider value={{value, setValue}}>
{children}
</MyContext.Provider>;
}
<MyContextProvider initialState={5}>
...
</MyContextProvider>
➡️ UserContext
- create a context for session with values:
- user
- username
- login = sets the user
- logout = sets user to null
- user
- create component CurrentUserInfo showing currently logged user & logout button
- ➡️ more info on the next slide
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>
}
type UserContext = {
user: User | null,
login: (user: User) => void;
logout: () => void;
}
const useUser = () => useContext(UserContext);
➡️ UserContext
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>
}
type UserContext = {
user: User | null,
login: (user: User) => void;
logout: () => void;
}
UserContextProvider
UserInfoPane
CurrentUserInfo
LoginForm
= missing
const useUser = () => useContext(UserContext);
❗️Impact on performance
- Change of the context triggers rerender
Testing
Testing
- unit/component testing
- integration testing
- e2e testing
Jest
- testing framework
- https://jestjs.io
- provides assertions, mocks, test runner
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);
})
export const add = (a, b) => a + b;
How to run tests?
- just execute jest
- to run them repeatedly: --watch
- looks for files named *.spec.js / *.test.js
Arrange / Act / Assert
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);
})
- arrange = prepare for tests (inputs, mocks...)
- act = run the tested unit
- assert = check the results (output, mocks)
Structuring tests
- describe
- beforeEach
- test
- test
- test
- describe
- beforeEach
- afterEach
- test
- describe
- test
- test
expect(add(1, 3)).toBe(4);
subject
matcher
Matchers
Matchers
- .toBe() = exact match (like ===)
- .toEqual() = deep equal
- .toBeNull(), toBeUndefined()
- .toContain() = is the item in an array?
- .toContainEqual()
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).toBe(4);
})
Negation
- .not.toBe()
- .not.toEqual()
- .not.toBeNull()
- ...
import {add} from './add';
it('adds numbers', () => {
expect(add(1, 3)).not.toBe(5);
})
Match substring
- toEqual(expect.stringContaining(...))
expect('How are you?').toEqual(expect.stringContaining('How'));
Match objects
- .toEqual({...})
- .toEqual(expect.objectContaining({ key: value }))
- .toHaveProperty(key, value)
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);
Match arrays
- .toEqual([...])
- .toEqual(expect.arrayContaining([...]))
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]));
Match exceptions
- expect( () => {...} ).toThrow()
- expect( () => {...} ).toThrow('Error message')
- expect( () => {...} ).toThrow(MyError)
expect(testedFn()).toThrow(); // ❌ DON'T
expect(() => testedFn()).toThrow(); // ✅ DO
Total number of asserts
- expect.assertions(2)
- used usually for async code
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)
})
➡️ Update package.json
{
"name": "...",
"jest": {
"transformIgnorePatterns": [
"/node_modules/(?!(axios))"
]
},
...
}
➡️ Update App.test.jsx
test.skip('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
Fix yellow warning
npm i -D @babel/plugin-proposal-private-property-in-object
➡️ Run tests
npm test
➡️ Test the division
- each test should test only one thing
- test the happy path
- test edge cases
export function divideWithRemainder(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero.');
}
const result = Math.ceil(a / b);
const remainder = a % b;
return {
result,
remainder
};
}
Mocks
Mocks
- also called spies
- lets you spy on the behavior
- can also replace existing behavior
- you should mock the boundary of your system
Creating a mock
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
- create mock
- define its behavior
Mock matchers
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
- evaluates if mock has been called
➡️ Test doNTimes
- use mocks
function doNTimes(fn, n) {
for(let i = 0 ; i < n; i++) {
fn();
}
}
Spy on existing
jest.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);
- possible only on instances of classes or objects
- spyOn(object, methodName)
Mocking modules
- used to mock 3rd party library
- jest.mock hoists up before all imports
import { fetchUser } from './fetch-user';
import axios from 'axios';
jest.mock('axios');
it('fetches user data', async () => {
const response = { data: { id: 1, name: 'Martin' } };
axios.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);
}
React testing
Testing React
- testing components, hooks
-
test should be as close as possible to how user will use the component
- test what he sees
- test how he interacts (mouse, keyboard etc)
- mock boundary of the system
- mock HTTP, local storage etc.
- Jest uses JSDOM to emulate browser
🔨 React testing library
- provides API for:
- querying the DOM
- doing user actions
- matchers
- https://testing-library.com/docs/
1. Queries
Query DOM
- get = 1 match immediately
- query = 0 or 1 match immediately
- find = 1 match, waiting
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 |
Query By
- Accessibility
- getByRole - by ARIA role
- getByLabelText = good for forms
- getByPlaceholderText = good for forms without labels
- getByText = what user sees, good outside of forms
- getByDisplayValue = value of an input
- HTML semantic
- getByAltText
- getByTitle
- Test IDs
- getByTestId = for invisible elements or dynamic text
import { screen } from '@testing-library/react';
screen.getByRole('button', {name: /submit/i});
Query by role
- check all ARIA roles
- default roles (W3 ARIA spec)
- button - "button"
- a - "link"
- h1...h6 - "heading"
-
accessible name
- usually what you want :-)
- label for form, alt for image...
getByRole('button', { name: 'Submit' });
// // checked input type checkbox
getByRole('checkbox', { checked: true });
// usually h2
getByRole('heading', { level: 2 });
Query by text
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'));
- what user sees
Query by test-id
<div class="background red" data-testid="background">
...
</div>
const backgroundEl = screen.getByTestId('background');
- test by HTML attribute data-testid
- used when the text is dynamic
- used when element has no content
Query within element
- useful when we want to scope query in specific area
import { screen, within } from '@testing-library/react';
const container = screen.getByTestId('container');
const helloMessage = within(container).getByText('hello');
waitFor
- waits for element to appear
- or disappear (
waitForElementToBeRemoved
)
- or disappear (
import { screen, waitFor } from '@testing-library/react';
await waitFor(() => {
expect(screen.getByText('Some text')).toBeInTheDocument();
});
Debugging tests
-
screen.debug()
screen.logTestingPlaygroundURL()
import { screen } from '@testing-library/react';
2. User actions
fireEvent
- fires DOM event on an element
- fires only a single event
- fireEvent.click doesn't trigger mouseDown, mouseUp
// click on an element
fireEvent.click(screen.getByText('Login'));
// change input value
fireEvent.change(getByLabelText(/username/i), {target: {value: 'martin'}});
3. Matchers
Matchers
- from jest-dom library
- makes tests easy to read
// 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();
Render component
import { render, screen } from '@testing-library/react';
it('renders', () => {
render(<App />);
const element = screen.queryByText('Hello');
expect(element).toBeInTheDocument();
});
➡️ Test UserInfoPane
1. render -> check LoginForm is visible
- test:
- current user
- login
- logout
Testing hooks
renderHook
- simulates component lifecycle
- prefer testing hook when testing the component
- act() simulates work of React
- wrap when you expect something to change
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 };
}
➡️ Test useCounter hook
export function useCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const randomize = () => setCount(Math.random());
return { count, increment, randomize };
}
Render props
Render props
Children
<WrapperComponent>
</WrapperComponent>
Render props
- wrapping component encapsulates the functionality
- composition pattern
function Component() {
return <MousePosition render={
(x, y) => <p>Position is x={x}, y={y}</p>
} />
}
function MousePosition({ render }) {
...
return <>{render(position.x, position.y)}</>;
}
Render props using children
function Component() {
return <div>
<Counter>
{({counter, increment}) => <>
<div>Counter value: {counter}</div>
<button onClick={increment}>INC</button>
</>}
</Counter>
</div>
}
function Counter({children}) {
const [counter, setCounter] = useState(0);
function increment() {
setCounter(counter + 1);
}
return <>{children({counter, increment})}</>
}
➡️ Create <JokeFetcher>
- create a component that fetches the joke
- it passes joke via render props
<JokeFetcher>
{({joke, isLoading}) => <p>{isLoading ? 'Loading...' : joke}</p>}
</JokeFetcher>
Error Boundary
Error boundary
- catches errors from render/component life cycle
- catches errors in Error Boundary children
- create-able only using the class component
- wrap any part of the app with error boundary
<JokeErrorBoundary>
<Joke />
</JokeErrorBoundary>
Error Boundary
import React from "react";
export class JokeErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// optional
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
const {error, hasError} = this.state;
if (hasError) {
// fallback
return <div>{String(error)}</div>;
}
return this.props.children;
}
}
➡️ Use ErrorBoundary
- create "error" component (code below)
- create a global error boundary
- when something bad happens it shows
- "Upss, something went wrong"
- + retry button
export function ThrowComponent() {
if (Math.random() < 0.5) {
throw 'Something went wrong.'
};
return <p>Didn't throw</p>
}
Routing
React router
- used to create multiple pages
- install react-router-dom
- npm i react-router-dom
- docs
Building blocks
- Layout page
- App.tsx
- router
Routable pages
Navbar
Layout.tsx
import { Outlet } from "react-router";
export function Layout() {
return <div>
<NavBar />
<Outlet />
</div>
}
- template for the root component
Here other routes render
Routable pages
<Outlet />
<Navbar />
App.tsx
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
... // (later)
]);
function App() {
return (
<RouterProvider router={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
Nested routes
- components can define subroutes
Routable pages
Navbar
<Outlet>
Routable pages
Navigation using links
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/articles">Articles</Link>
<Link to="/articles/{articleId}">Specific article</Link>
Imperative navigation
const navigate = useNavigate();
navigate('/articles');
- using useNavigate() hook
Reading url parameters
import { useParams } from "react-router-dom";
function Joke() {
const params = useParams();
return (
{params.category}
);
}
/path-segment/:pathParam/something?query1=one&query2=two
useSearchParams
useParams
➡️ Create multiple pages
- UserInfoPane should be always visible
- NavBar should be always visible
- Create routes
- /votes -> album voting
- /categories -> categories list
- /categories/:category -> joke from category
- load joke based on category
GET https://api.chucknorris.io/jokes/random?category={category}
SWR
stale-while-revalidate
SWR
- library for caching HTTP requests
- cache shared across all components
- npm i swr
import useSWR from 'swr'
const fetcher = (...args) =>
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>
);
}
Invalidate SWR cache
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>;
}
isValidating vs isLoading
- isLoading = fetching & not loaded yet
- isValidating = fetching (can be loaded from the last time)
Suspense
Suspense
- used to wait for something to finish
- usually, we wait for the data to be loaded
- or for JS chunk to load
- Suspense wraps component which gets suspended
function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ComponentWithLoading />
</Suspense>
)
}
How does it work?
- suspendable component must be wrapped with <Suspense>
- suspendable component throws an exception with a Promise in the render
- until the promise is resolved, Suspense shows fallback
- when the promise resolves, Suspense tries to render the component again
➡️ Suspend & SWR
- fetch joke using useSWR
- enable Suspense support for SWR
- wrap Joke component with Suspense showing "Loading with Suspense"
const { data, isValidating } = useSWR(url, fetcher, { suspense: true });
const { data, isValidating } = useSWR(url, fetcher);
Split app into chunks
Chunks
- by default we have only main chunk
- contains the whole app
- better to usually split the app by routes
- user doesn't have to load code he doesn't need
- vendor chunk = for 3rd party libraries
How to create chunk
- combine React.lazy & import()
// 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>
);
}
Support in React Router
- chunk per route
- expects key Component
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
React Portal
Portal
- used to render JSX anywhere in the DOM
- everything works as if the JSX was rendered as normal
- useful for modal dialog (z-index, positioning)
createPortal
- first argument - JSX
- second argument - DOM element
import {createPortal} from 'react-dom';
function Component() {
return <div>
{createPortal(<div>Hello</div>, document.body)}
</div>
}
➡️ Create a modal dialog
- create a component that renders children in a modal window
- position it in the middle of the screen
<Modal>
<h1>Hello</h1>
<p>I am modal</p>
</Modal>
Strict mode
Strict mode
- checks for common mistakes
- only in dev mode
- runs effects twice
- renders components twice
➡️ What's wrong?
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>
))}
</>;
}
React hooks
- ✅ useState
- ✅ useEffect
- ✅ useContext
- ✅ useRef
- ✅ useImperativeHandle
- ✅ useCallback
- ✅ useMemo
- useReducer
useReducer
- alternative to useState
- better for complex logic
- good when there are several useState depending on each other
- "simple" redux built-in React
Example: counter
function reducer(state, 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
➡️ useReducer in UserContext
- write reducer with following actions
- user login
- user logout
-
session timeout
- 10 seconds after login, the session times out and logs the user out
- show how much time is left
type State = {
user: User | null;
timeLeft: number
}
type Action = LOGIN | LOGOUT | SESSION_TICK;
Redux
Redux
- state management library
- one global store
- big object
- actions to modify the state
- browser extension: Redux Devtools
- npm install @reduxjs/toolkit react-redux
Store
Actions
Reducers
Store example
{
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;
}
1. Create slices
// /src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
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;
2. Create a store
// src/store/store.js
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
},
});
3. Provide the store
// index.jsx
import { store } from './store/store'
import { Provider } from 'react-redux'
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
4. Read the state in a component
import { useSelector } from 'redux-toolkit';
function Counter() {
const count = useSelector(state => state.counterSlice.count);
return <div>Current count: {count}</div>
}
- a selector should transform data into a shape for the component
5. Dispatch actions
import { useDispatch } from 'redux-toolkit';
import { increment } from "../store/counterSlice";
function IncrementButton() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}
When to use Redux state?
- global state
- Is it used by multiple components?
- Do you need the state after a component unmounts?
- Do you want it to work with timetravel?
- Do you want to cache it?
➡️ Move state to Redux
- move UserContext state from the local context state into Redux
Redux & async operations
- run async => dispatch action when finished
-
createAsyncThunk
- create async function which has access to getState & dispatch
- automatically dispatches actions pending/fulfilled/rejected
// 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
}
)
What happens when finished?
// 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) => {
if (
state.loading === 'pending'
) {
state.loading = 'idle'
state.joke = action.payload
}
})
)
});
extraReducers = for dealing with actions defined somewhere else
How to call fetchJoke?
const dispatch = useDispatch();
dispatch(fetchJoke(category));
➡️ Use redux for Joke
- move Joke to redux state
- use createAsyncThunk to load the joke
React eco system
How to choose a library?
- measurable metrics
- number of downloads, github stars, github issues, repository activity (opened PRs, merged PRs...)
- stackoverflow questions
- documentation
- migration guide
- how often does it have breaking changes
- Proof of concept
- does it work for us? How easy is to use? How easy is it to learn?
A typical project
- router (react-router)
- state management (redux, MobX, xstate...)
- CSS library, design system framework
- data fetching (axios, React Query)
- form library (Formik,React Hook Form )
- run & build (webpack, vite)
- test (react-testing-library, jest)
- check awesome-react for libraries
Webpack
Webpack
- module bundler
- anything you import

webpack.config.js
const 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'}),
],
};
Chunks example
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'}),
]
};
NX
- tooling for monorepo
- apps + libraries
- define tasks for each app/library
- collection of commands
- nx affected - runs task only for changed parts
- plugins for adding storybook, generating e2e tests...
Dependency graph
- npx nx graph
- you can define boundaries of packages
- what can import what
🎉
React Advanced Javascript July 2024
By Martin Nuc
React Advanced Javascript July 2024
- 212