Masaryk University

What is React?

In today's lesson, we will explore the React library. We will learn how it works, its fundamental principles, and how to create and work with components. We will also connect React to TypeScript and learn the basic constructs for defining a user interface using TSX.


React is a JavaScript library for building user interfaces. A UI consists of small elements such as buttons, text, or images. With React, we can create small, reusable UI parts called components to build complex, maintainable web interfaces. These components are the building blocks of an application, enabling us to assemble user interfaces quickly. React's component-based architecture promotes code reuse and better organization.

Examples of reusable Material UI components

Single Page Application (SPA)

A Single-Page Application (SPA) is a web application or website that dynamically updates the current page rather than loading entirely new pages from the server. The initial page load retrieves the necessary HTML, JavaScript, and CSS; additional resources may be loaded as needed, usually in response to user actions. The document does not perform a full page reload; JavaScript dynamically updates the content and DOM.

Comparison of navigation in a multi-page application and a single-page application

User Experience - An SPA can provide smooth transitions and rich interactivity. When users perform an action, the page can update without a full refresh.

AJAX - SPAs usually utilize AJAX (Asynchronous JavaScript and XML). Although application resources are loaded initially or on demand, applications still need to retrieve and submit data.

Client-side Rendering - An SPA can update the UI in the browser using data fetched from a server. React itself is a UI library; a framework such as Next.js can also provide server-side rendering.

Routing - In the traditional approach, navigating to each URL requests a new document from the server. In an SPA, JavaScript can handle routing without a full document request, although it may still fetch data or code for the destination.

When to use

The Single Page Application approach has its use cases. We can mention information systems, where all application pages are behind authentication. However, for websites that are publicly accessible to anyone and that fetch data from public APIs, an SPA might not be a good option.

Disadvantages

SEO considerations - Content loaded dynamically by JavaScript can make indexing and sharing metadata more difficult, especially for crawlers that do not execute JavaScript. Server-side rendering can address these concerns.

Initial Load Performance - Since all of the resources for the app need to be retrieved once at the beginning, it might quickly become slow. It does not matter in some applications, but on the e-commerce page, it might be a big deal.

React component tree

The resulting UI, composed of components, is organized in a hierarchical tree structure in React. This structure ensures data flows in only one direction through the props. The child component inside the tree cannot modify the data it has received from its parent. If we need a child component to be able to modify the data or state of its parent, the parent must provide a function for the child to call to modify the data. Each component defines what will be displayed on the screen where the component is placed or called based on its state and the props received.

Component tree showing one-way data flow from parent components to children
type ChildComponentProps = {
  onClick: () => void;
};

function ChildComponent({ onClick }: ChildComponentProps) {
  return <button onClick={onClick}>Click me</button>;
}

function ParentComponent() {
  const onClick = () => {
    console.log('Button clicked in parent');
  };

  return <ChildComponent onClick={onClick} />;
}

The React component tree plays a vital role in updating the user interface. If a component changes state or gets new props, it must be re-rendered to the screen, and at the same time, all children of that component must be re-rendered as well. This is where the advantage of the tree structure comes in, where it is easy to see which components on the screen need to be updated. At the same time, React uses the VDOM, where it compares its in-memory stored DOM of the current and previous version, and then when it detects that the component has changed not only its state or its props but also its actual UI, it updates it in the browser DOM.

Diagram comparing React's virtual DOM with the browser DOM

React component

A component is a UI building block that combines with other components to create a complete interface. Modern React components are typically TypeScript functions with the following characteristics:

Historically, components were written as classes that extended React.Component. Function components are now the standard approach in React.

A component can be a single small button that can be extended and modified in various ways via props, or it can be an entire page.

Props

Props allow the parent to pass read-only data to the child component to modify its appearance or behavior. It is a fundamental feature of components that will enable you to create reusable components.

Basic example of component accepting props:

type MyComponentProps = {
  onClick: () => void;
  label: string;
};

function MyComponent(props: MyComponentProps) {
  const onClick = props.onClick;
  const label = props.label;

  return <button onClick={onClick}>{label}</button>;
}

With ES6 destructuring, we can destructure the props object directly in the function parameters.

type MyComponentProps = {
  onClick: () => void;
  label: string;
};

function MyComponent({ onClick, label }: MyComponentProps) {
  return <button onClick={onClick}>{label}</button>;
}

We can also extend existing props with additional props. For example, React already defines props for <button>, and we can extend them with label.

type MyComponentProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  label: string;
};

function MyComponent({ label, ...props }: MyComponentProps) {
  return <button {...props}>{label}</button>;
}

When using a component as a wrapper, meaning we want to include content inside it, we can use the children prop. This special prop contains the component's children. React provides the PropsWithChildren type to include it in a props type.

type MyComponentProps = React.PropsWithChildren<{
  color: string;
}>;

function MyComponent({ children, color }: MyComponentProps) {
  return <button className={`${color}`}>{children}</button>;
}

JSX (TSX)

JSX is a JavaScript syntax extension used in React to declare the UI that a component should display. It allows HTML-like syntax directly in JavaScript or TypeScript files. In addition to standard HTML tags, JSX can include component invocations such as <MyComponent props1="props1" /> and JavaScript expressions enclosed in curly braces, such as <span>{totalCount}</span>.

Rules for writing JSX/TSX

Importing and exporting components

It is common practice to put components in their files. It would be tough to navigate if we had all the components of an application in one file. Therefore, we need a mechanism by which we can export and import components (or other functions and constants). With the advent of ES6, we have the following options:

Export

There are two options: named and default exports. A file can use both approaches.

Import

All components are imported right at the beginning of the file.

The named exported component can be renamed within the import:

import { MyComponent as MyComponentWithDifferentName } from './MyComponent';

Folder structure

There is no single best way to organize components within an application. Later in the semester, we will use the Next.js framework, which uses the folder structure for routing. Developers can organize other components as they see fit. With React alone, a multi-page application needs a router from an external library, and components can be organized as needed.

Rendering

Rendering is the process of React calculating the UI from components and elements. When a component's state or props change, React renders it again and reconciles its subtree. Unless the resulting UI changes, React does not update the browser DOM.

Conditional rendering

A frequently used construct within TSX is conditional rendering. This is the rendering of a component or element under a condition.

Example using &&:

{isLoggedIn && <LogoutButton />}

The logout button is only rendered if isLoggedIn is a truthy value. If isLoggedIn is passed via props and its value changes, it will trigger a rerender, and since a chunk of the UI depends on this variable, the DOM will also be updated.

Example of using the ternary operator:

{isLoggedIn ? <LogoutButton /> : <LoginButton />}

If we want a component not to render anything based on a condition, it is typical to use an if statement.

function Banner({ hidden }: { hidden: boolean }) {
  if (hidden) {
    return null;
  }

  return <div>Banner!</div>;
}

An alternative is to use the ternary operator again.

function Banner({ hidden }: { hidden: boolean }) {
  return hidden ? null : <div>Banner!</div>;
}

Mapping over array items

It is often necessary to render several identical components based on a provided array of items. To do this, we use functional methods over an array, mainly the map function.

{items.map(item => <Item key={item.id} {...item} />)}
type Item = { id: string; label: string };

function MyListComponent({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <span>{item.label}</span>
        </li>
      ))}
    </ul>
  );
}

When mapping over elements, it is essential to provide a key explicitly. key is a special React attribute, not a prop that the component receives. Keys help React identify which items have changed, been added, or been removed. Stable, unique keys allow React to preserve the correct component state when items are reordered.

Incorrect usage of keys

const IncorrectUsageOfKeys = () => {

  const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);

  return (
    <ul>
      {items.map((item, index) => (
         <li key={index}>
           <input type="text" />
           <span className="ml-2">{item}</span>
         </li>
      ))}

      <li>
        <button
          onClick={() => setItems(items.toReversed())}
        >
          Reverse
        </button>
      </li>
    </ul>
  );
}

  • Item 1
  • Item 2
  • Item 3

Fixed usage of keys

const CorrectUsageOfKeys = () => {
  
  const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);

  return (
    <ul>
      {items.map((item) => (
         <li key={item}>
           <input type="text" />
           <span className="ml-2">{item}</span>
         </li>
      ))}

      <li>
        <button
          onClick={() => setItems(items.toReversed())}
        >
          Reverse
        </button>
      </li>
    </ul>
  );
}

  • Item 1
  • Item 2
  • Item 3

Keeping components pure

First, let us define a pure function in programming:

  1. Deterministic Output: The output of a pure function is solely determined by its input values. Given the same input, a pure function will always return the same output.
  2. No Side Effects: Pure functions do not cause side effects. This means they don't alter external state, variables, or data outside the function scope. They also don't rely on any external state.

React components in the context of pure functions:

These properties make components deterministic, predictable, and reusable. Side effects belong outside rendering, for example in event handlers or Effects. Data from external sources can change over time; once that data becomes a component input, the component should still render predictably for the given input.

Responding to events

React lets you add event handlers to TSX. Event handlers are custom-defined functions triggered in response to user interactions such as clicking, hovering, or focusing form inputs.

Built-in elements, such as buttons and anchors, support browser events such as onClick. When you create your own components, you can define custom event-handler props alongside other component props. These event handlers can have custom names, for example onCardClick.

React provides a cross-browser event system that normalizes browser events for React applications.

Basic example

function MyComponent() {
  const onClick = () => {
    console.log('Button clicked');
  };

  return <button onClick={onClick}>Click me</button>;
}

We define onClick as an arrow function and pass it to the <button> element as its event handler. Unlike a function declaration, an arrow function assigned to a const cannot be called before its declaration. This is not possible:

function MyComponent({ param }: { param: string | undefined }) {
  if (param === undefined) return null;

  onClick();
  // ^--- Error: Block-scoped variable 'onClick' used before its declaration.

  const onClick = () => {
    console.log(param);
  };

  return <button onClick={onClick}>Click me</button>;
}

If we used a function declaration, this would be possible.

function MyComponent({ param }: { param: string | undefined }) {
  if (param === undefined) return null;

  onClick();

  function onClick() {
    console.log(param);
  }

  return <button onClick={onClick}>Click me</button>;
}

This behavior is called hoisting. Function declarations are available throughout their enclosing scope before their textual declaration.

Arrow functions can also preserve TypeScript's control-flow narrowing. In this example, TypeScript infers param as string within onClick, rather than string | undefined, because the component returns early when param is undefined.

// TypeScript
function MyComponent({ param }: { param: string | undefined }) {
  if (param === undefined) return null;

  const onClick = () => {
    console.log(param);
    //            ^---- : string
  };

  return <button onClick={onClick}>Click me</button>;
}

Child to parent communication

Often, you want a child component to communicate back to the parent. We can do this by passing functions from the parent to the child that the child can call.

type ChildComponentProps = {
  onChildClick: (data: string) => void;
};

function ChildComponent({ onChildClick }: ChildComponentProps) {
  return <button onClick={() => onChildClick('Child data')}>Click me</button>;
}

function ParentComponent() {
  const onChildClick = (data: string) => {
    console.log('Received from child:', data);
  };

  return <ChildComponent onChildClick={onChildClick} />;
}

Native events

When we work with native elements such as <button>, their event handlers receive events. React provides a SyntheticEvent object with a cross-browser interface. We can call methods such as stopPropagation() or preventDefault() on this event. For example, submitting a form triggers a page navigation by default. Call preventDefault() in the submit handler to prevent this behavior.

import type { FormEvent } from 'react';

const PreventDefaultExample = () => {
  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    // prevent browser refresh
    event.preventDefault();

    console.log(event.currentTarget.surname.value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="surname" />
      <button type="submit">Submit</button>
    </form>
  );
};

Next lecture

In the next lesson, we'll look at component libraries and general web application styling options. We will also discuss basic accessibility concepts that every web developer should follow concerning people with disabilities.

Assignment

This week, create a React page in Next.js to display and add GitHub repositories. Build a form for new entries and a list to show existing ones, with validation and conditional messages.