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.
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.
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.
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.
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:
- Return TSX or
null. - Optionally accept a key-value object called props as a parameter.
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
- JSX must always have just one root element.
- All tags must be closed.
- Because JavaScript has some rules for naming variables, not all attribute HTML tags match how they are written in HTML. So the rule in JS is that all attributes with a hyphen in their name are written
camelCasein JSX. For example,stroke-width->strokeWidth. Then there are exceptions, such as the very commonly usedclassName, which replaces the original HTML tag attributeclass, which is a reserved word in JS.
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.
-
Named - we can export multiple components from one file.
export const MyComponent = () => { ... }; -
Default - we can export only one component from a file. When we subsequently import such a component, we can assign any name to it.
export default MyComponent;
Import
All components are imported right at the beginning of the file.
-
Importing a named export (multiple components)
import { MyComponentOne, MyComponentTwo } from './MyComponent'; -
Importing a default export
import MyComponentWithDifferentName from './MyComponent';
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:
- 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.
- 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:
- Props are inputs to components. A component may also depend on values that do not change during rendering, such as module-level constants. For the same inputs, it should return the same output.
- No side effects during rendering. Components should not modify global variables or other external state while rendering.
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.