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

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
      • email
    • login = sets the user
    • logout = sets user to null
  • 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

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

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

  1. 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
  2. HTML semantic
    • getByAltText
    • getByTitle
  3. 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)
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

Building blocks

  1. Layout page
  2. App.tsx
  3. 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?

  1. suspendable component must be wrapped with <Suspense>
  2. suspendable component throws an exception with a Promise in the render
  3. until the promise is resolved, Suspense shows fallback
  4. when the promise resolves, Suspense tries to render the component again

➡️ Suspend & SWR

  1. fetch joke using useSWR

     
  2. enable Suspense support for SWR

     
  3. 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