React

JSX

DOM APIs

const renderArticle = (
    title: string,
    author: string,
    content: string
) => {
    const articleEl = document.createElement("article");
    const titleEl = document.createElement("h2");
    titleEl.innerText = title;
    articleEl.appendChild(titleEl);
    const authorEl = document.createElement("p");
    authorEl.setAttribute("class", "author");
    authorEl.innerText = "By " + author;
    articleEl.appendChild(authorEl);
    const contentEl = document.createElement("div");
    contentEl.innerHTML = content;
    articleEl.appendChild(contentEl);
    return articleEl;
};

What does this function do? Do you like it?

What if

What if we could generate our HTML elements directly from JavaScript, but with an HTML-like syntax?

Entering JSX

const renderArticle = (
    title: string,
    author: string,
    content: string
) => {
    return (
        <article>
            <h2>{title}</h2>
            <p className="author">By {author}</p>
            <div>{content}</div>
        </article>
    );
};

Example 1 Hello react

Exercise 1 Random book

Goal

Display a random book title form the following list:

const books: string[] = [
    "Anna Karenina",
    "To Kill a Mockingbird",
    "The Great Gatsby",
    "One Hundred Years of Solitude",
    "A Passage to India",
    "Invisible Man",
    "Don Quixote",
    "Beloved",
    "Mrs. Dalloway",
    "Things Fall Apart",
    "Jane Eyre",
    "The Color Purple",
];

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/01-hello-react example to a folder 03-react/01-random-book in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Step 2 Random book

  • In main.ts, write a getRandomBook() function that returns a random book from the array. Your function should have an explicit return type annotation.
  • Update the React App component to display a random book on every page load.

Step 3 Random color

  • In main.ts, write a getRandomColor() function that returns a random color. Your function should have an explicit return type annotation.
  • Use the getRandomColor() function to also randomly change the background-color of your book title.

Conditional rendering

Reference

Conditional rendering, React docs

If-else

function App() {
    const userConnected: boolean = false;

    if (userConnected) {
        return <button>Show my profile</button>;
    }
    return <button>Log in</button>;
}

Conditional operator

function App() {
    const userConnected: boolean = false;
    return (
        <div className={userConnected ? "guest" : "user"}></div>
    );
}

Lists

Reference

Lists and key, React docs

Using .map()

const books: string[] = [
    "Anna Karenina",
    "To Kill a Mockingbird",
];

const App = () => (
    <ul>
        {books.map(title => <li>{title}</li>))}
    </ul>
);

Keys

When rendering a list, each element of the list should have unique key prop.

const books: string[] = [
    "Anna Karenina",
    "To Kill a Mockingbird",
];

const App = () => (
    <ul>
        {books.map(title => <li key={title}>{title}</li>))}
    </ul>
);

Components

Reference

Components and Props, React docs

Defining a component

function Comment() {
    return (
        <article>
            <h2>My comment</h2>
            <p className="author">By Matt</p>
            <p>I think that…</p>
        </article>
    );
}

function App() {
    return <Comment />;
}

Props

interface CommentProps {
    title: string;
    author: string;
    content: string;
}

function Comment(props: CommentProps) {
    return (
        <article>
            <h2>{props.title}</h2>
            <p className="author">By {props.author}</p>
            <p>{props.content}</p>
        </article>
    );
}

function App() {
    return (
        <Comment
            title="Evidence"
            author="René"
            content="Cogito, ergo sum"
        />
    );
}

Object as prop

interface CommentInfo {
    title: string;
    author: string;
    content: string;
}

interface CommentProps {
    comment: CommentInfo;
}

function Comment(props: CommentProps) {
    return (
        <article>
            <h2>{props.comment.title}</h2>
            <p className="author">By {props.comment.author}</p>
            <p>{props.comment.content}</p>
        </article>
    );
}

function App() {
    const discourse: CommentInfo = {
        title: "Evidence",
        author: "René",
        content: "Cogito, ergo sum",
    };

    return <Comment comment={discourse} />;
}

Example 2 Books list

Exercise 2 Users list

Goal

Display the following list of users in two sections Adults and Kids:

const users = [
    {
        id: 1,
        avatar:
            "https://cdn.fakercloud.com/avatars/nicollerich_128.jpg",
        first_name: "Claire",
        last_name: "Price",
        age: 12,
        city: "North Kirstin",
        ip: "161.208.247.105",
        isAdmin: false,
    },
    {
        id: 2,
        avatar:
            "https://cdn.fakercloud.com/avatars/edobene_128.jpg",
        first_name: "Carrie",
        last_name: "Mayer",
        age: 62,
        city: "Everettetown",
        ip: "123.224.60.146",
        isAdmin: true,
    },
    {
        id: 3,
        avatar:
            "https://cdn.fakercloud.com/avatars/rodnylobos_128.jpg",
        first_name: "Jessy",
        last_name: "Kassulke",
        age: 8,
        city: "Lake Fordhaven",
        ip: "164.89.151.58",
        isAdmin: false,
    },
    {
        id: 4,
        avatar:
            "https://cdn.fakercloud.com/avatars/charliegann_128.jpg",
        first_name: "Amalia",
        last_name: "Rogahn",
        age: 108,
        city: "South Russberg",
        ip: "162.25.24.120",
        isAdmin: false,
    },
    {
        id: 5,
        avatar:
            "https://cdn.fakercloud.com/avatars/jmfsocial_128.jpg",
        first_name: "Kristoffer",
        last_name: "Wuckert",
        age: 66,
        city: "Lake Angelina",
        ip: "85.179.25.149",
        isAdmin: false,
    },
    {
        id: 6,
        avatar:
            "https://cdn.fakercloud.com/avatars/craigrcoles_128.jpg",
        first_name: "Kiera",
        last_name: "Rohan",
        age: 36,
        city: "East Beverly",
        ip: "27.34.243.77",
        isAdmin: false,
    },
    {
        id: 7,
        avatar:
            "https://cdn.fakercloud.com/avatars/bruno_mart_128.jpg",
        first_name: "Emmie",
        last_name: "Hand",
        age: 74,
        city: "New Maryjaneberg",
        ip: "85.232.129.225",
        isAdmin: true,
    },
    {
        id: 8,
        avatar:
            "https://cdn.fakercloud.com/avatars/woodsman001_128.jpg",
        first_name: "Cameron",
        last_name: "Veum",
        age: 16,
        city: "Altadena",
        ip: "168.252.158.225",
        isAdmin: false,
    },
    {
        id: 9,
        avatar:
            "https://cdn.fakercloud.com/avatars/markwienands_128.jpg",
        first_name: "Tremaine",
        last_name: "Sanford",
        age: 10,
        city: "Lenexa",
        ip: "38.83.7.48",
        isAdmin: false,
    },
    {
        id: 10,
        avatar:
            "https://cdn.fakercloud.com/avatars/ah_lice_128.jpg",
        first_name: "Ronaldo",
        last_name: "Weissnat",
        age: 83,
        city: "Baumbachtown",
        ip: "50.218.254.52",
        isAdmin: false,
    },
    {
        id: 11,
        avatar:
            "https://cdn.fakercloud.com/avatars/zaki3d_128.jpg",
        first_name: "Ephraim",
        last_name: "Goyette",
        age: 63,
        city: "North Martina",
        ip: "147.105.193.65",
        isAdmin: false,
    },
    {
        id: 12,
        avatar:
            "https://cdn.fakercloud.com/avatars/boxmodel_128.jpg",
        first_name: "Clay",
        last_name: "Kunde",
        age: 113,
        city: "South Damon",
        ip: "209.104.243.157",
        isAdmin: false,
    },
    {
        id: 13,
        avatar:
            "https://cdn.fakercloud.com/avatars/VinThomas_128.jpg",
        first_name: "Melisa",
        last_name: "Leannon",
        age: 18,
        city: "Hilo",
        ip: "103.82.167.168",
        isAdmin: false,
    },
    {
        id: 14,
        avatar:
            "https://cdn.fakercloud.com/avatars/miguelmendes_128.jpg",
        first_name: "Clovis",
        last_name: "Medhurst",
        age: 15,
        city: "Harveybury",
        ip: "168.223.235.220",
        isAdmin: false,
    },
    {
        id: 15,
        avatar:
            "https://cdn.fakercloud.com/avatars/mandalareopens_128.jpg",
        first_name: "Mylene",
        last_name: "Renner",
        age: 49,
        city: "Arlington",
        ip: "223.89.148.36",
        isAdmin: false,
    },
    {
        id: 16,
        avatar:
            "https://cdn.fakercloud.com/avatars/ma_tiax_128.jpg",
        first_name: "Marcos",
        last_name: "Ferry",
        age: 47,
        city: "Strackehaven",
        ip: "74.94.165.210",
        isAdmin: false,
    },
    {
        id: 17,
        avatar:
            "https://cdn.fakercloud.com/avatars/balakayuriy_128.jpg",
        first_name: "Brain",
        last_name: "Mohr",
        age: 54,
        city: "Carrollton",
        ip: "11.121.113.44",
        isAdmin: false,
    },
    {
        id: 18,
        avatar:
            "https://cdn.fakercloud.com/avatars/aleclarsoniv_128.jpg",
        first_name: "Bella",
        last_name: "VonRueden",
        age: 18,
        city: "Columbia",
        ip: "224.144.68.251",
        isAdmin: true,
    },
    {
        id: 19,
        avatar:
            "https://cdn.fakercloud.com/avatars/andrewarrow_128.jpg",
        first_name: "Franz",
        last_name: "Raynor",
        age: 28,
        city: "Garrickchester",
        ip: "91.159.111.88",
        isAdmin: false,
    },
    {
        id: 20,
        avatar:
            "https://cdn.fakercloud.com/avatars/carlosgavina_128.jpg",
        first_name: "Celestino",
        last_name: "Bailey",
        age: 61,
        city: "Aronport",
        ip: "242.25.16.144",
        isAdmin: false,
    },
];

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/01-hello-react example to a folder 03-react/02-users-list in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Step 2 UserInfo interface

Write a UserInfo interface, and add an explicit type annotation to the users constant.

Step 3 All users list

Update the App component so that it displays all users (no component and no filtering yet).

Step 4 User component

Extract the code that shows a single user to a User component in src/components/User. The component should have a single prop of type UserInfo.

Step 5 Separate adults and kids sections

Display the users in two separate sections depending on their age.

Step 6 Admin badge

Next to the name of each user, display a badge if it is an admin.

Recap ES6 destructuring

Reference

Array destructuring

// This is a shorthand:
const [hello, world] = ["Hello", "World"];

// for this:
const myArray = ["Hello", "World"];
const hello = myArray[0];
const world = myArray[1];

Object destructuring

// This is a shorthand:
const { firstname, lastname } = {
    firstname: "Ada",
    lastname: "Lovelace",
};

// for this:
const myObject = { firstname: "Ada", lastname: "Lovelace" };
const firstname = myObject.firstname;
const lastname = myObject.lastname;

Events

Reference

Handling Events

onClick prop

To listen on clicks:

<button onClick={() => console.log("Button clicked!")}>
    Click me!
</button>

onChange prop

To listen on input changes:

return (
    <input
        type="text"
        onChange={(e) =>
            console.log("New value: " + e.target.value)
        }
    />
);

Type of onChange event

As in the DOM API, the handler takes an event object as parameter, with a target property referring the to event target (HTML element on which the event was triggered).

When you write the handler inline as in the previous slide, you do not need a parameter type annotation. However, if you want to declare your function elsewhere, you will need to tell TypeScript that the event is of type React.ChangeEvent<HTMLInputElement>:

const handleChange = (
    e: React.ChangeEvent<HTMLInputElement>
) => console.log("New value: " + e.target.value);
return <input type="text" onChange={handleChange} />;

For now, let’s assume React.ChangeEvent<HTMLInputElement> represents “an object with a target property of type HTMLInputElement”. We will learn about generics later.

useState hook

Reference

Using the State Hook

Usage

const App = () => {
    // `0` is the initial value, before `setCounter` is ever called.
    const [counter, setCounter] = React.useState(0);

    // We can read the state value using `counter`, or
    // change it by calling `setCounter(newValue)`.

    return (
        <main>
            <p>{counter}</p>
            <button onClick={() => setCounter(counter + 1)}>
                Increment
            </button>
        </main>
    );
};

React will take care of automatically re-rendering the component when the state changes.

In this case, the view will be updated when calling setCounter().

Example 3 Counter

Example 4 Addition

Exercise 3 Random book on click

Goal

On every click, display a different book, with a different background color.

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/01-hello-react example to a folder 03-react/03-random-book-on-click in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Exercise 4 Degrees converter

Goal

Reproduce your degrees converter in React.

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/01-hello-react example to a folder 03-react/04-converter in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Recap: fetch API and promises

Reference

Usage with then() and catch()

fetch("https://api.quotable.io/random")
    .then((response) => {
        console.log("HTTP Response status: " + response.status);
        return response.json();
    })
    .then((data) => console.log(data));

Usage with async and await

const getMyQuote = async () => {
    const response = await fetch(
        "https://api.quotable.io/random"
    );
    console.log("HTTP Response status: " + response.status);
    const data = await response.json();
    console.log(data);
};

Adding query parameters to a URL

const url = new URL("https://api.example.com");
url.searchParams.append("q", "Hello World");
url.searchParams.append("sort", "year");
console.log(url.toString());

Type of response.json()

response.json() returns an object of type any:

const data: any = await response.json();

This means that you can assign it to any type, and TypeScript will not check it. This is because TypeScript could not possibly know what will be returned by your request. Therefore, your need to be extra careful to assign it to the correct type.

Typed useState()

You can explicitly set the type of a React state value:

const [value, setValue] = React.useState<boolean>(true);

Example 5 Random quote

Goal

On your page, there should be a text input and a Search images button. When clicking on the button, you should show 10 related images found with the Pixabay API.

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/05-random-quote example to a folder 03-react/05-images-search in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Step 2 Find the search API method

Find which API method you will need to call to get your results. Try an example in your browser and study the JSON response.

Step 3 Write a PixbaySearch type

Write a PixbaySearch interface corresponding to the data you expect back from your request to the API.

Note: you only need to declare the object properties that you will use.

Step 4 Define a state of type PixbaySearch | null

To create a state variable of type PixbaySearch | null, which an initial value of null, use:

const [data, setData] =
    React.useState<PixbaySearch | null>(null);

Step 5 Add the click event listener

Add a minimal view that just shows your data state variable as JSON (JSON.strinfify(data)) in a <pre> element, and a button that will call the API and update the state using setData(...).

Step 6 Write the view

Write the actual code displaying your data:

  • Before the button is clicked, “Type something and click ‘Search images’” should be displayed.
  • If the button has been pressed but not images have been found, you should display “No images found”.
  • If the button has been pressed and images have been found, show the images.

Recap Arrow functions

Reference

Array functions expressions > Syntax, MDN

Shortest form

const pow2 = (n: number): number => n * n;

Here, parentheses are optional.

Multiple or no parameters

const pow2 = (): string => "Hello world";
const add = (a: number, b: number): number => a + b;

Here, parentheses are mandatory.

Multiline statements

const greet = (name: string): string => {
    const result = `Hello ${name}!`;
    return result;
};

To write several statements in the body of the function, we must add brackets around the body and a return statement.

Function types

const greet = (
    name: string,
    transform: (name: string) => string
): string => {
    const transformedName = transform(name);
    const result = `Hello ${transformedName}!`;
    return result;
};

The syntax for function types is similar to array functions

Mini-exercise Types of array methods

Think how to specify the type of f in these functions:

  • map(array: number[], f: ???): apply f to each item.
  • filter(array: number[], f: ???): only keep elements where f(element) returns true.
  • reduce(array: number[], f: ???): f takes two parameters, the current accumulated value, and an item. It is uses to reduce the array to a single value.

Mini-exercise reduce usage

Use Array.prototype.reduce() to:

  • compute the sum of the elements in an array,
  • compute the minimum element in an array,
  • find the first non-null element in an array.

Example implementation of map()

Example implementation of map() for an array of numbers:

const map = (
    numbers: number[],
    f: (n: number) => number
) => {
    const result: number[] = [];
    for (const n of numbers) {
        result.push(f(n));
    }
    return result;
};

map([1, 2, 3], (x) => x * x);
// Returns [1, 4, 9]

map([1, 2, 3], (x) => x * 2);
// Returns [2, 4, 6]

Example implementation of filter()

Example implementation of filter() for an array of numbers:

const filter = (
    numbers: number[],
    f: (n: number) => boolean
) => {
    const result: number[] = [];
    for (const n of numbers) {
        if (f(n)) {
            result.push(n);
        }
    }
    return result;
};

filter([1, -5, 6, -9, 2, 3], (x) => x >= 0);
// Returns [1, 6, 2, 3]

filter([0, 5, 8, 2, 9, 3], (x) => x % 2 === 0);
// Returns [0, 8, 2]

Example implementation of reduce()

Example implementation of reduce() for an array of numbers:

const reduce = (
    numbers: number[],
    f: (a: number, b: number) => number,
    initialValue: number
) => {
    let acc = initialValue;
    for (const n of numbers) {
        acc = f(acc, n);
    }
    return acc;
};

reduce([1, 2, 3], (a, b) => a + b, 0);
// Returns 6

reduce(
    [2, 1, 3],
    (a, b) => Math.min(a, b),
    Number.MAX_SAFE_INTEGER
);
// Returns 1

Object and array state updates

State in React is considered immutable

const [numbers, setNumbers] = React.useState([1, 2, 3]);

numbers[2] = 42;
setNumbers(numbers); // THIS DOES NOT WORK!

Referential equality examples

[] === []; // false
[1, 2, 3] === [1, 2, 3]; // false
{"firstname": "Ada"} === {"firstname": "Ada"}; // false
{} === {};  // false

const a = [1, 2, 3];
const b = a;
a === b;  // true

b.push(42)
a === b // true

Referential equality video

Array slice method

See Array.prototype.slice().

Array spread syntax

const a = [1, 2, 3];
const b = [...a, 6];

Append to an array

function append<T>(array: T[], value: T) {
    return [...array, value];
}

Prepend to an array

function append<T>(array: T[], value: T) {
    return [value, ...array];
}

Remove an array element

function remove<T>(array: T[], index: number) {
    return [
        ...array.slice(0, index),
        ...array.slice(index + 1),
    ];
}

Replace an array element

function replace<T>(array: T[], value: T, index: number) {
    return [
        ...array.slice(0, index),
        value,
        ...array.slice(index + 1),
    ];
}

Object spread syntax

const ada = { firstname: "Ada", lastname: "Lovelace" };
const ada2 = { ...ada, age: 42 };

Exercise 6 Form

Goal

Write a <Form /> component with a single state variable of the following type:

interface UserForm {
    firstname: string;
    lastname: string;
    age: number;
}

Your component should show one <input> for each property of the UserForm interface, a submit button. On form submission (using onSubmit event), your show the content of the fields to the page.

Please work on a 03-react/06-form directory in your exercises repository.

Example 6 Numbers sum

Exercise 7 To-do list

Goal

Reproduce your todo list with React.

Step 1 Add the React boilerplate

  • Copy the content of 03-react/examples/01-hello-world example to a folder 03-react/07-todo-list in your exercises repository.
  • Then, run npm install.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Step 2 State type

Define a TodoItem type representing an individual todo list item. It should an object with the following attributes:

  • created (a number),
  • done (a boolean),
  • title (a string).

The created attribute will hold the timestamp of the creation time of the item and will be used as a unique ID. See Date.now().

Step 3 State type

Define an initial example state in a initialTodos variable of type TodoItem[].

Step 4 todos state variable

Edit the App component so that it contains a state variable todos, with the initial value initialTodos.

Step 5 Display the list

Render the todos variable into a list, with a checkbox for each item.

Step 6 Checkbox handlers

Add event handlers so that the checkbox can be checked/unchecked, and that the state is updated accordingly.

Step 7 Add item form

Add a form with a single input allowing to add an item.

Step 8 (extra) Persistence

Save the state of your app to local storage.

Step 9 (extra) Filter

Add a <select> element allowing to filter items by “All”, “To do” or “Done”.

useEffect hook

Reference

Using the Effect Hook, React docs

Run once the component is mounted

useEffect(() => console.log("Component did mount."), []);

Run when a state variable changes

const [text, setText] = React.useStat("");
useEffect(
    () => console.log("State variable 'text' changed."),
    [text]
);

React router

Reference

React Router documentation

Basic routing

export default function App() {
    return (
        <Router>
            <div>
                <nav>
                    <ul>
                        <li>
                            <Link to="/">Home</Link>
                        </li>
                        <li>
                            <Link to="/about">About</Link>
                        </li>
                    </ul>
                </nav>

                {/* A <Switch> looks through its children <Route>s and
            renders the first one that matches the current URL. */}
                <Switch>
                    <Route path="/about">About</Route>
                    <Route path="/">Home</Route>
                </Switch>
            </div>
        </Router>
    );
}

URL Parameters

function User() {
    const params = useParams<{ id: string }>();

    return <p>{param.id}</p>;
}

export default function App() {
    return (
        <Router>
            <div>
                <nav>
                    <ul>
                        <li>
                            <Link to="/">Home</Link>
                        </li>
                        <li>
                            <Link to="/user/1">User 1</Link>
                        </li>
                        <li>
                            <Link to="/user/2">User 2</Link>
                        </li>
                    </ul>
                </nav>
                <Switch>
                    <Route path="/user/:id" children={<User />}>
                        About
                    </Route>
                    <Route path="/">Home</Route>
                </Switch>
            </div>
        </Router>
    );
}

Exercise 8 Powercoders participants

Goal

  • On the homepage, show a list of contributors to the 2021-1-web-dev repo.
  • On route /participant/:name, show information about a single participant with GitHub username :name.

Details