React Basic in Typescript

🙋‍♂️

Frontend engineer

  • 9 years FE experience
  • AngularJS, Angular, Ember, React
  • currently Staff engineer at Productboard

❤️ 📸

❤️ 🛞

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

Slides

GitHub

Your experience?

Setup IDE

Install

Javascript & Typescript

Let's talk about React! 💪

React

  • library for managing view
  • component based
    • helps split the app into small pieces
  • used to create SPA

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

Vite

Vite

  • tool for scaffolding react app
  • replaced create-react-app
npm create vite@latest

Project name: … (react-course)
Select a framework: › React
Select a variant: › Typescript

Important parts

package.json

  • describes the package
  • dependecies list
  • npm scripts

tsconfig.json

  • settings for typescript compiler
  • "target" = build for version of JS

index.html

  • HTML
  • notice div id="root"

/public folder

  • contains assets which are not referenced in the source

main.tsx

  • renders React into HTML element
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

App.tsx

  • the main component

➡️ Start the React app

npm run dev

JSX

Elements

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

What is JSX?

  • syntactic sugar around createElement
  • almost like HTML
  • transpiled to Javascript
  • example in App.tsx:
import React from 'react';

function App() {
  return (
    <div className="App">
      Hello
    </div>
  );
}
function App() {
  return React.createElement('div', { className: 'App' }, 'Hello');
}

Q: Why className?

import React from 'react';

function App() {
  return (
    <div className="App">
      Hello
    </div>
  );
}
function App() {
  return React.createElement('div', { className: 'App'}, 'Hello');
}

Q: What happens now?

import React from 'react';

function App() {
  return (
    <div>
      Yes
    </div>
    <div>
      No
    </div>
  );
}
function App() {
  return ????????
}

Solution: React Fragment

  • like empty element
  • when you want to return multiple elements - wrap them in fragment
import React from 'react';

function App() {
  return (
    <>
      <div>
        Yes
      </div>
      <div>
        No
      </div>
    </>
  );
}

➡️ Before we continue

  • remove everything in the body of App.tsx component
  • notice the browser reloads
function App() {
  return <h1>Hello</h1>;
}

Print a variable

function App() {
  let something = 'hello';
  
  return <div>{something}</div>;
}

Print an array

function Array() {
  let array = [1,2,3];
  
  return <div>
    {array.map((item, index) => <span key={index}>{item}</span>)}
  </div>;
}

‼️ key prop

Components

Component

  • reusable unit
  • just a function
  • input
    • ="props"
  • output
    • React element
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 tree

  • split big problems to smaller ones

Component tree

Component

Component

Component

Component

Component

Component

Component tree

  • Stateful components (smart)
    • used to fetch data
    • data manipulation
  • Stateless components (dumb)
    • only display data
  • pass data down, emit events up

Component tree

Component

User info

ArticleList

Article

Today Weather

Article

I am smart 💡

Stateless component

  • everything to display is received via props
  • just a function
    • input: props (=properties)
    • output: React element
  • easy to test
function NameComponent(props) {
  return <h1>{props.name}</h1>;
}

How to use a component?

  • pass data down via props
function App() {
  return <NameComponent name="Martin" />
}

Event handling

  • React unifies API of events (link)
<button type="button" onClick={() => console.log('Hello')}>
  Hello world
</button>

➡️ Dynamic table [old]

  • 1️⃣ create a component that renders an array as a HTML table in a single row
  • 2️⃣ create a component that renders an array as a HTML table with a specific number of columns
  • receives the array and number of columns in props
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️⃣

➡️ Vending machine [final]

Tatranka

Fidorka

Mars

Price: 20,-

Coins: 50,-

🪙

Insert coin

3x

5x

0x

out of stock

➡️ Vending machine

Tatranka

Fidorka

Mars

➡️ N-buttons

  • show available items
  • click on the button console.logs name
  • label all buttons: "1x"

Tatranka

Fidorka

Mars

function VendingMachine() {
  const items = [
	'Tatranka',
    'Fidorka',
	'Mars'
  ];
  
  return ...
}

function VendingItem() {
  return <button />{name}
}

1x

1x

1x

State

useState

  • hook for storing data
  • instead of declaring variable
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

➡️ Create counter [old]

  • create button with counter as text
  • start from 0
  • everytime you click the button the counter is increased

➡️ Vending machine

Tatranka

Fidorka

Mars

3x

5x

0x

➡️ Create goods counter

  • each vending machine item should show a count of available items
  • start with 5x items each
  • when clicked, it should decrease the number
  • the number cannot be negative

Component lifecycle

  • mounted
  • updated
    • triggered by change of state
    • triggered by change of props
    • ➡️ render
  • unmounted

Class components

  • rarely used nowadays
  • uses a class instead of a function
  • this.props
  • this.setState() to change state
  • life cycle hooks
    • componentDidMount
    • componentWillUnmount

Counter example

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>
  }
}

➡️ Rewrite class component as a functional component

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>
  }
}

Important things to notice

  • setter needs a new reference
    • we cannot use .push
  • the initial set is generated on every render

Conditions

  • use if statement
  • use ternary operator
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>}
  </>
}

➡️ Vending machine

Tatranka

Fidorka

Mars

3x

5x

0x

out of stock

➡️ Show out of stock

  • item with 0 count should show it's out of stock
<div style={{color: 'red'}}>Out of stock</div>

Debugging

Main tools

debugger;

Chrome dev tools

  • Network
  • Source
  • Performance
  • Application
  • React dev tools
    • Components
    • Profiler

Logging

  • Sentry.io
  • TrackJS

Styling app

Import CSS

  • global CSS
  • can use preprocessors (SCSS, SASS)
import './App.css';

function Component() {
  return <div className="red">Hello</div>
}
.red {
  color: red;
}

App.css

App.tsx

CSS modules

  • scoped CSS
  • can use preprocessors (SCSS, SASS)
  • css file must be named .module
import styles from './Component.module.css';

function Component() {
  return <div className={styles.red}>Hello</div>
}
.red {
  color: red;
}

App.module.css

App.tsx

Conditional styling without CSS modules

  • classnames library
  • npm i classnames @types/classnames
  • key = class to be applied
  • value = condition
import cn from 'classnames';

function ValidationState() {
  const [invalid, setInvalid] = useState(false);
  
  return <div className={cn({ red: invalid })}>
    Status
  </div>
}

Conditional styling with CSS modules

  • dynamic keys
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>
}

➡️ Vending machine

Tatranka

Fidorka

Mars

3x

5x

0x

out of stock

➡️ Out of stock

  • when out of stock (count = 0)
    • apply a red background on the button
    • disable the button
  • use CSS modules

useEffect

useEffect

  • hook for side effects
    • = things not related to render
  • second argument say when it runs
    • empty - on every render
    • [ ] - only at the begining (=on mount)
    • [ variable ] - when a variable changes
  • should return cleanup function

useEffect example

  • tracks mouse position
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>
  );
};

➡️ Create automatic counter [old]

  • create a component which increases the counter every second
  • in the parent component create a button which shows/hides automatic counter component

➡️ Track where user clicks

  • when the user performs a mouse click, save the coordinate
  • show clicks history under the vending machine

Data down, events up

Events up

Component

User info

ArticleList

Article

Today Weather

Article

I am smart, I know what to do 💡

❤️ "User liked an article"

Creating own event

  • component emits event up
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:

➡️ Vending machine

Tatranka

Fidorka

Mars

3x

5x

0x

out of stock

Price: 20,-

➡️ Move state to the vending machine component

  • information about count should be a concern of the Vending machine
  • when a user clicks the button, it tells the vending machine to adjust the count
  • show total price

Children props

Children props

  • you might pass HTML as body of element:
<MyBytton onClick={...}>
  <Icon> Click me!
</MyBytton>
  • Table component receives react element via children prop:
function MyButton(props) {
  return (
    <button className="blue-button" onClick={props.onClick}>
      {props.children}
    </button>
  )
}

➡️ Create a generic dropdown

  • What is dropdown?
  • button which opens a menu when clicked
  • props:
    • label = label of the button
    • children = the dropdown content
<DropdownComponent label="Open dropdown">
  Hello, this is dropdown!
</DropdownComponent>

➡️ Vending machine

Tatranka

Fidorka

Mars

🪙

Insert coin

3x

5x

0x

out of stock

+10 coins

+20 coins

+50 coins

Coins: 50,-

Price: 20,-

➡️ Add coins

  • Show current coins
  • Use the generic background to show buttons to insert coins
<DropdownComponent label="Insert coins">
  <button>+ 10 coins</button>
  <button>+ 20 coins</button>
  <button>+ 30 coins</button>
</DropdownComponent>

Controlled input

  • use component state as the only source of truth
function Component() {
  const [inputName, setInputName] = useState(name);

  return <>
    {inputName}
    
    <input
      value={inputName} 
      onChange={(e) => setInputName(e.target.value)} />
  </>
}

➡️ Vending machine

Tatranka

Fidorka

Mars

🪙

Insert coin

3x

5x

0x

out of stock

+10 coins

+20 coins

+50 coins

Any amount

Coins: 50,-

Price: 20,-

➡️ Create input

  • input of type number
  • how much the counter will increment
  • on enter it adds the amount

+10 coins

+20 coins

+50 coins

Any amount

➡️ Vending machine

Tatranka

Fidorka

Mars

Price: 20,-

Coins: 50,-

🪙

Insert coin

3x

5x

0x

out of stock

Strict mode

Strict mode

  • checks for common mistakes
  • only in dev mode
  • runs effects twice
  • renders components twice

API request

Axios library

Axios POST usage

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));
  • good idea to trigger in useEffect - why?

Typed response

type Response = {
  id: number;
  name: string;
  age: number;
}

axios.get<Response>('/api/users/1')

➡️ Let's make http request 

  • open API request in browser to see structure of response
  • ​​1️⃣ display joke in the component
  • 2️⃣ create a button to load another joke
GET https://api.chucknorris.io/jokes/random

Custom hooks

Custom hooks

  • separate logic from view
  • no render
  • named use*
  • hooks to component lifecycle
  • clear API

useMouseMove

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 };
};
  • mouse position example
    • no input
    • outputs x, y of mouse

Fetch joke hook

  • 1️⃣ encapsulate fetching joke logic into a custom hook
    • think about API first
  • 2️⃣ Improvement: remember already fetched jokes, load next adds into the list

React Context

Context

  • "global" state for subtree of components
  • avoids passing props deep
  • Provider + Consumer
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>;
}

Context

  • pass object with value + setter
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>
  </>;
}

➡️ Vending machine

Tatranka

Fidorka

Mars

Price: 20,-

Coins: 50,-

🪙

Insert coin

3x

5x

0x

out of stock

➡️ Light switch 💡

  • style vending machine based on a light switch
    • light is on - bright
    • light is off - dark
  • create a light switch button

useRef

useRef

  • manipulate with DOM elements
  • object with mutable current property
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>
}

➡️ Autofocus input

  • make the input for coins focused on mount

Testing

Testing

  • unit/component testing
  • integration testing
  • e2e testing

Jest & vitest

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);
})

How to run tests?

  • just execute vitest
  • 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.tsx

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
"scripts": {
  "test": "vitest"
}
npm install -D vitest

➡️ Test the division

  • each test should test only one thing
  • test the happy path
  • test edge cases
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
  };
}

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

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);
  • possible only on instances of classes or objects
  • vi.spyOn(object, methodName)

Mocking modules

  • used to mock 3rd party library
  • jest.mock hoists up before all imports
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);
}

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.
    • uses JSDOM to emulate browser

🔨 React testing library

➡️ Install

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'

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)
  • prefer findBy*
import { screen, waitFor } from '@testing-library/react';

await waitFor(() => {
  expect(screen.getByText('Some text')).toBeInTheDocument();
});

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 } from '@testing-library/react';

it('renders', () => {
  render(<App />);
  const element = screen.queryByText('Hello');
  expect(element).toBeInTheDocument();
});

Debugging tests

  • screen.debug()

  • screen.logTestingPlaygroundURL()
import { screen } from '@testing-library/react';

➡️ Test UserInfoPane

1. render -> check LoginForm is visible

  • test:
    • current user
    • login
    • logout

➡️ Test Joke component

  • mock the server response
  • test:
    • loading state
    • the fetched joke is displayed
    • button to load the next joke
    • button should be disabled when fetching
{
  "name": "...",
  "jest": {
    "transformIgnorePatterns": [
      "/node_modules/(?!(axios))"
    ]
  },
  ...
}

package.json

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: number) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const randomize = () => setCount(Math.random());

  return { count, increment, randomize };
}

Routing

React router

  • used to create multiple pages
  • install react-router-dom + type definitions
    • npm i react-router-dom @types/react-router-dom
  • docs

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

  • NavBar
    • should be always visible
    • links to home & categories routes
  • UserContextPane should be always visible
  • Create routes
    • /-> what we had before
    • /categories -> categories list
    • /categories/:category -> joke from a category

Sources

// 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>
}

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

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

➡️ 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.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;

2. Create a store

// 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;

3. Provide the store

// index.tsx
import { store } from './store/store'
import { Provider } from 'react-redux'

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

4. Created typed hooks

// 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;
  • avoid using useDispatch & useSelector
  • create copies typed by your store

5. Read the state in a component

import { useAppSelector } from '../store/hooks';

function Counter() {
  const count = useAppSelector(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 { useAppDispatch } from '../store/hooks';
import { increment } from "../store/counterSlice";

function IncrementButton() {
  const dispatch = useAppDispatch();
  
  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?

Formik

Formik

  • library for forms
  • supports schema validation
    • uses library yup
  • docs
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>
  );
}

 ➡️ Create a form

  • create registration form using formik
  • include validations
  • fields
    • email
    • password (at least 8 chars)

🎉

React basic course December 2024

By Martin Nuc

React basic course December 2024

  • 98