Skip to main content
Version: 2024sp

Frontend / Unit 2

Welcome to Unit 2!

This unit will cover the basics of frontend development using React, a popular frontend framework.

Homework: HW3 and HW4, due Mar 25 and Apr 8 respectively.

Slides: Here

How React?

Allows developers to create reusable UI components and manage the state of those components efficiently. React uses a virtual DOM (Document Object Model) to improve performance by minimizing the amount of DOM manipulation required when a user interacts with a React application. This allows for efficient updates and rendering of components, making it a popular choice for building complex and high-performing web and mobile applications.

In essence, it's built around a few core features:

  • Reusable components
  • Reactivity (state management)

Conceptually, everything in React is a function. Your entire UI is therefore a function of your state. This is a very powerful concept.

Components

Components are the building blocks of visual React. They are reusable pieces of code that can be used to build more complex components. These functional components form the visual basis of all React applications. They are defined as functions that return a React element. They are the simplest way to define a component.

Here, an example of a functional component (implemented as an arrow function):

const MyComponent = () => {
return <div>Hello World!</div>;
};

Then, elsewhere, you can reuse MyComponent, such as in a higher-order App.tsx component:

const App = () => {
return (
<div>
<MyComponent />
</div>
);
};

The returned code is JSX, which is a JavaScript extension that allows you to write HTML-like code in JavaScript. It is compiled to JavaScript by Babel, and then inserted as HTML into the DOM.

Thus, we can compose components together to build more complex components. This is the basis of React's component-based architecture, which is compositional. This is a very powerful concept, and we'll cover it in more detail later in the next lecture, once you have a more firm grasp of basic React.

Hooks

Hooks are the building blocks of logical React. They are reusable pieces of code that can be used to manage the state of your application, and are the most powerful feature of React. Changes to a hook variable will cause components and other hooks that depend on it to re-render or re-calculate their values, which is the basis of React's reactivity.

Like for components, you can create custom hooks to encapsulate logic and reuse it across your application. However, extremely frequently, you'll also use the built-in hooks provided by React.

The most important hook is useState, which simply provides a getter and setter for a variable. This is the basis of React's state management.

Here, an example of a component that uses useState:

const MyComponent = () => {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};

The other most important built-in hook is useEffect, which allows you to run code when a component is mounted or unmounted, or when a variable changes. This is the basis of React's lifecycle management.

Lifecycle management is when you want to run code when a component is mounted or unmounted, or when a variable changes, or on every render. It encapsulates logic that must in essence run at certain points during a component's lifecycle.

Here, an example of a component that uses useEffect:

const MyComponent = () => {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('The count changed!'); // this will fire every time count variable changes
}, [count]);

useEffect(() => {
console.log('The component mounted or the count changed!'); // this will fire every render (all the time)
});

useEffect(() => {
console.log('The component mounted!'); // this will fire once, when the component is mounted
return () => {
console.log('The component unmounted!'); // this will fire once, when the component is unmounted
};
}, []);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};

Combining components and hooks, you can build complex applications with React that respond to events, update their state accordingly, and then re-render the UI to reflect the new state!

Today, let's build an extremely simple React app to get a feel for how React works. Next time, we'll use useState, useEffect, and components more extensively, along with other React features.

Introducing Complexity into our React Apps

Component #1: ContactCard

ContactCard.tsx
type Props = { readonly name: string; readonly githubLink: string };

const ContactCard = ({ name, githubLink }: Props) => (
<div>
You can reach {name} at
<a href={githubLink}>{githubLink}</a>
</div>
);

What! What's going on??

Functional Component

Recall: the simplest component in React is a functional component. A functional component does not have any internal state. You can think of it as a function whose inputs are some JavaScript object and the output is some HTML code that is generated from the data.

In React, we call the JavaScript object props, so you can see code like this:

ContactCard.tsx
type Props = { readonly name: string; readonly githubLink: string };

const ContactCard = (props: Props) => (
<div>
You can reach {props.name} at
<a href={props.githubLink}>{props.githubLink}</a>
</div>
);

Just calling the input props is not good for documentation purpose, so we commonly use object destructuring to make it more explicit:

ContactCard.tsx
type Props = { readonly name: string; readonly githubLink: string };

const ContactCard = ({ name, githubLink }: Props) => (
<div>
You can reach {name} at
<a href={githubLink}>{githubLink}</a>
</div>
);

Modules

In order for this component to be reused in another file, we need to export it:

ContactCard.tsx
type Props = { readonly name: string; readonly githubLink: string };

const ContactCard = ({ name, githubLink }: Props) => (
<div>
You can reach {name} at
<a href={githubLink}>{githubLink}</a>
</div>
);

export default ContactCard;

This is because React is organized around the concept of modules. A module is a collection of components that are related to each other. In our case, we have a module called ContactCard that contains a single component called ContactCard. In order to use this component in another file, we need to export it (make it publicly available to other modules).

Stateful Component

Imagine you are writing a contacts app and you need to implement an editor.

Unlike the previous components, you need to maintain state. In React, you will need hooks. Hooks are functions that use state and lifecycle methods inside functional components. The useState hook is the hook for maintaining state. Note that the general naming convention of a hook is useXXXX.

import { useState, ChangeEvent } from 'react';

const NewContact = () => {
// name is the variable for the state, setName is the function you can use
// to change the state.
const [name, setName] = useState('');
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
// To extract the value from input box, use the following line.
const n = event.currentTarget.value;
setName(n);
};
return (
<div>
<p>Name: {name}</p>
<input
type="text"
placeholder="enter the name here"
value={name}
onChange={handleChange}
/>
</div>
);
};

export default NewContact;

useState returns a length-2 array that includes the following elements (in order):

  1. a state variable that is always synchronized (in a consistent state everywhere any time)
  2. a function that can be used to update the state variable.

Note that the names of the two should always be in the form x, setX.

const [prosAndCons, setProsAndCons] = useState([]);

Note that the useState statement above uses array destructuring syntax.

Common use-case one: Rendering lists

You may want to render a list of YourAwesomeComponent. Here are some examples to show how you can achieve this in different ways.

// Suppose you have a ContactCard component defined there.
import ContactCard from './ContactCard';

type Contact = { name: string; githubLink: string };

const data: Contact[] = [
{ name: 'Jason', githubLink: 'www.github.com/guessJason' },
{ name: 'Peter', githubLink: 'www.github.com/peterIsCool' },
{ name: 'Enoch', githubLink: 'www.github.com/eno' },
];

const ListBySimpleMap = () => (
<div>
{data.map((contact: Contact) => (
<ContactCard
key={contact.name}
name={contact.name}
githubLink={contact.githubLink}
/>
))}
</div>
);

const ListBySimpleMapWithObjectDestructing = () => (
<div>
{data.map(({ name, githubLink }) => (
<ContactCard key={name} name={name} githubLink={githubLink} />
))}
</div>
);

const ListBySimpleMapWithSpread = () => (
<div>
{data.map((contact: Contact) => (
<ContactCard key={contact.name} {...contact} />
))}
</div>
);

Note that we always need a key prop. Without this, React will give you warnings in the console. React needs a unique key for each item in the list to help it avoid rerendering everything when only one item in the list changes. In this particular example, you should only use name as the key if you know that the property name is unique. However, if there are multiple objects with the same name in the list that are used as a key, it would confuse React.

Common use-case two: Conditional rendering

Sometimes we only want things to render when a certain condition is met. For example, only display text when we meet a certain condition. React has conditional rendering to make this very simple.

PrelimTime.tsx
import React from 'react';

export default ({ prelimToday }: { readonly prelimToday: boolean }) => {
if (prelimToday) {
return <p>I have a prelim today.</p>;
} else {
return <p>I don't have a prelim today.</p>;
}
};

In this example, we have a functional component PrelimTime that takes in a prop prelimToday. prelimToday is a boolean holding whether we have a prelim today or not. We want the component to display "I have a prelim today." if prelimToday is true and "I don't have a prelim today." if it is false.

Traditionally, we would use the if statement for this behavior (as shown above). We can also use conditional rendering to make writing this functionality more convenient.

First we can use the ternary operator (remember this?):

PrelimTime.tsx
import React from 'react';

export default ({ prelimToday }: { readonly prelimToday: boolean }) => (
prelimToday
? <p>I have a prelim today.</p>
: <p>I don't have a prelim today.</p>;
);

The ternary operator is also very common in other languages as well such as Java or Python. The basic syntax is as follows:

[boolean expression] ? [true_result] : [false_result]

Before the ? you have your expression producing true or false. The part after the ? but before the : is the result/functionality you want if the boolean expression evaluates to true. The part after the : is what you want to happen if the expression is false.

Connecting with the PrelimTime example, my boolean expression was just the prop prelimToday, although in your code it can be a more complex boolean expression. If prelimToday is true, I display "I have a prelim today." If prelimToday is false, I display "I have a prelim today."

Notice though, how the only thing changing in this text is adding the word "don't" if prelimToday is false. So only if prelimToday is false, we want to add don't.

React supports the use of && operator (remember this?):

PrelimTime.tsx
import React from 'react';

export default ({ prelimToday }: { readonly prelimToday: boolean }) => (
<p>I {!prelimToday && "don't"} have a prelim today.</p>
);

Here, we display the text "I have a prelim today.". However, in the curly braces, if prelimToday is false then the word "don't" will be rendered. Conditional rendering with && is useful when you only have expected behavior for one branch of the conditional. In this case, I only had desired behavior if prelimToday was false.

As you have seen, React's conditional rendering made modifying render behavior based on conditions a lot easier. In this small example, we went from five lines of code in the component to just one!

Modifying State

We can use the useEffect hook. Using useEffect + setStateVar (state variable setter) allows state variables to โ€œhook intoโ€ the React component and โ€œride alongโ€ other changes that occur. ๐Ÿค ๐Ÿ‡ Here is how!

useEffect(effect_function) => useEffect(() => {}) Whenever the component updates/re-renders, useEffect runs the argument (a function).

useEffect(() => {setCount(count + 1)}) The function can have any arbitrary logic/function callsโ€ฆ such as the setCount state variable update function! But setCount also triggers another component update soโ€ฆ

Optimizing useEffect

useEffect(function, filters) useEffect triggers the function at every component update, but you can restrict this to occur only when the variables in the filters array update. This makes your React component more optimized. You could say that these variables are a dependency of the useEffect statement. Example below:

useEffect(function, [prop1, observable])

As a fun fact, it is possible to enter an infinite loop if the dependencies are state variables that are also set inside the effect. Don't do this!

Aside: Lifecycle Cleanup

A good use of useEffect is to hook into file streams, WebSockets, Firebase hooks, or some other Observable-like API in order to make your component reactive to changes in data. (when the observed data/value/file stream updates, the React component should update.) In order to use an API for this purpose, it is often necessary to open up an initial connection or subscription. It is good manners to cleanup by closing or unsubscribing. In a useEffect statement, the cleanup code is stored in a function that is returned by the effect (function).

useEffect(() => {
return () => {
cleanup();
};
});

Example usage below:

useEffect(() => {
return () => {
ObservableAPI.unsubscribe()
});
}, [valueFromObservableApi]);

useEffect(() => {
return () => {
dataStream.close()
});
}, [dataStreamContents]);

What we have so far

import { useState, useEffect } from 'react';

type Props = { readonly name: string; readonly githubLink: string };

const ContactCard = ({ name, githubLink }: Props) => (
<div>
You can reach {name} at
<a href={githubLink}>{githubLink}</a>
</div>
);

// -----

const NewContact = () => {
// name is the variable for the state, setName is the function you can use
// to change the state.
const [name, setName] = useState('');
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
// To extract the value from input box, use the following line.
const n = event.currentTarget.value;
setName(n);
};
return (
<div>
<p>Name: {name}</p>
<input
type="text"
placeholder="enter the name here"
value={name}
onChange={handleChange}
/>
</div>
);
};

// -----

type Contact = { name: string; githubLink: string };

const data: Contact[] = [
{ name: 'Jason', githubLink: 'www.github.com/guessJason' },
{ name: 'Peter', githubLink: 'www.github.com/peterIsCool' },
{ name: 'Enoch', githubLink: 'www.github.com/eno' },
];

const ContactList = () => (
<div>
{data.map((contact: Contact) => (
<ContactCard
key={contact.name}
name={contact.name}
githubLink={contact.githubLink}
/>
))}
</div>
);

// -----

const App = () => {
const [showNewContactDialog, setShowNewContactDialog] = useState(false);
return (
<div>
<button onClick={() => setShowNewContactDialog(!showNewContactDialog)}>
{showNewContactDialog ? 'Hide' : 'Show'}
</button>
{showNewContactDialog && <NewContact />}
<ContactList />
</div>
);
};

Note the concepts of Composition and Inheritance we promised we'd come back to last lecture!

Composition vs. Inheritance

Composition and inheritance are two programming techniques for defining how classes relate to objects. (Think of classes as the blueprint for a house and objects the actual houses created from that blueprint)

Composition

Composition defines a class as the sum of its individual parts. This is a "has-a" relationship (e.g. a car has a steering wheel, has a window, etc). In Java (and other object oriented languages), these components are represented as instance variables.

Inheritance

Inheritance derives one class from another. If class A is the parent of class B and C, B and C inherit the properties/functions of A. This is a "is-a" relationship (e.g. car is a vehicle, circle is a shape.)

React uses Composition

โ€œReact has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components.โ€ -- React Docs

Containment

Components may not know their children ahead of time.

Children are the components you put within another component:

<ComponentA>{/* anything here is a child of Component A */}</ComponentA>

Use the children prop to pass in children components.

Container.tsx
import React, { ReactNode } from 'react';
type Props = { readonly children: ReactNode };
const Container = (props: Props) => (
<div className="Border">{props.children}</div>
);
const App = () => (
<div className="App">
<Container>
<p>Hello!</p>
<p>Bye!</p>
</Container>
</div>
);

props.children will have the paragraph elements.

We didn't actually get to this live demo, adapted from this tutorial in the React docs, during lecture but it is very simple if you want to try it out yourself. We also show how to import styles.

Container.tsx
import React, { ReactNode } from 'react';
import './Container.css'; // this is how we import styles

type Props = { readonly children: ReactNode };

export default (props: Props) => <div className="Border">{props.children}</div>;
Container.css
.Border {
border: 4px solid black;
background-color: azure;
}

Less common but you also may want multiple "holes" in your component (for example, a left and right child):

SplintPane.tsx
import React, { ReactNode } from 'react';
import './SplitPane.css';

type Props = { readonly left: ReactNode; readonly right: ReactNode };

export default (props: Props) => (
<div>
<div className="LeftPane">{props.left}</div>
<div className="RightPane">{props.right}</div>
</div>
);
SplitPane.css
/* these colors are ugly I know */
.LeftPane {
float: left;
width: 50%;
background-color: red;
}

.RightPane {
float: right;
width: 50%;
background-color: aquamarine;
}
import React from 'react';
import SplitPane from './SplitPane';
import Container from './Container';

export default () => {
return (
<div className="App">
<Container>
<p>Hello, world!</p>
</Container>
<SplitPane
left={<div>I'm on the left!</div>}
right={<div>I'm on the right!</div>}
/>
</div>
);
};

Now, let's add more functionality!

There's a few hooks we haven't yet covered, which add additional functionality to React.

useRef

useRef allows you to create a "ref" to an element or a variable in your component. This can be useful for accessing the DOM or for keeping track of a value that should persist across renders.

Here's an example of using useRef to create a ref to an input element:

import { useRef } from 'react';

function InputExample() {
const inputRef = useRef<HTMLInputElement>(null);

const handleClick = () => {
inputRef.current!.focus();
};

return (
<div>
<input ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}

In this example, we're creating a ref to an <input> element using useRef. We pass null as the initial value, which is the default. Then we're passing the ref to the <input> element using the ref attribute. We can access the input element using inputRef.current and call focus() on it.

useCallback

useCallback allows you to memoize a callback function, so that it will only be recreated if one of its dependencies has changed. This can be useful for preventing unnecessary re-renders in child components.

Here's an example of using useCallback to memoize a callback function:

import { useCallback } from 'react';

function Parent({ data }) {
const handleClick = useCallback(
(index) => {
console.log(data[index]);
},
[data],
);

return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
const items = [1, 2, 3];

return (
<div>
{items.map((item, index) => (
<button key={item} onClick={() => onClick(index)}>
{item}
</button>
))}
</div>
);
}

In this example, we're using useCallback to create a memoized version of the handleClick function, which takes an index argument and logs the corresponding data from the data prop. The second argument to useCallback is an array of dependencies, in this case, just data. We're passing the memoized handleClick function as a prop to the Child component. When the buttons in the Child component are clicked, the memoized handleClick function is called with the corresponding index, and it logs the corresponding data from the data prop, without re-creating the function and re-rendering the child component.

useMemo

useMemo is a useful hook that can help you improve the performance of your component by reducing the amount of unnecessary calculations.

Syntax: const result = useMemo(func, deps)

func is an "expensive" calculation that we want to memoize

deps is the list of dependencies (just like in useEffect)

In essence, the hook will call func initially and put whatever it returns into result. Then ONLY when something in deps changes does func gets called again - otherwise result will be the memoized return value. Whenever such a refresh occurs, the new return value of func will overwrite the old memo.

Here is an example of where you might want to useMemo:

const expensiveFunction = (n: number) => {
/** do something that takes a lot of cpu */
};

const RandomComponent = () => {
const [foo, setFoo] = useState(0);
const [bar, setBar] = useState(0);

// This runs expensiveFunction when foo changes but bar doesn't
const baz = expensiveFunction(bar);

// This runs expensiveFunction ONLY when bar changes
const baz = useMemo(() => expensiveFunction(bar), [bar]);
};

IMPORTANT PITFALL: You may be tempted to put useMemo everywhere; however, this is not a good idea. Every hook has some performance overhead, so adding useMemo in places where you don't need it can actually worsen performance!

You can profile your code with and without the useMemo call to judge whether it's a good idea. You can profile the performance of your website using the Developer Tools found in most browsers.

useContext

We've covered passing down props in previous React lectures. However, that's pretty annoying if every component within a hierarchy needs that prop.

Is there a better way then manually passing down that prop to every component that needs it?

Yes!

The useContext hook allows you to wrap an entire component tree with a "context" that every component in that tree can access!

A great use case for this hook is for theme data - each component needs to know which theme is selected in order to display the correct colors, for example.

Here is the example pulled from the official React docs

const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};

const ThemeContext = React.createContext(themes.light);

const App = () => {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
};

const Toolbar = () => {
return (
<div>
<ThemedButton />
</div>
);
};

const ThemedButton = () => {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
};

Using Hooks!

Naming

Hooks have the following naming scheme: useXXXX (camelCase). It is imperative that you name your hooks using this scheme - the function name is the only way to identify the function as a hook to other developers as well as your IDE.

It is also a good idea to avoid prefixing regular variable names with use, to avoid confusion.

Top Level

Hooks (both built-in and custom hooks) can only be called within React components or other React hooks. More specifically, they should only be called in the top level of such functions.

The reason for this is that you want hooks to be called in the same order, the same amount of times each time the function runs. This restriction is necessary for React to optimize the performance of hooks.

This means that you should not call hooks in:

  • conditionals
  • loops
  • nested functions

Here are some examples of what not to do (and would trigger linter errors):

const RandomComponent = () => {
const [foo, setFoo] = useState(0); // this is fine
if (foo < 100) {
const [bar, setBar] = useState(0); // this is NOT fine
}
};
const RandomComponent = () => {
const [foo, setFoo] = useState(0); // this is fine
for (let i = 0; i < foo; i++) {
const [bar, setBar] = useState(0); // this is NOT fine
}
};
const RandomComponent = () => {
const doHookStuff = () => {
const [bar, setBar] = useState(0); // this is NOT fine
};
doHookStuff();
};

It's a good practice to call all your hooks line-by-line at the top of your function.

Custom Hooks

There are many hooks that React gives us out of the box, but we can put them together to make our own hooks!

This is useful to abstract out common functionality, the same way programmers do with regular functions.

If you ever notice that you are doing repetitive tasks with hooks across multiple React components, it might be a good idea to put all that logic into your own hook.

Syntax for Custom Hook

Just write a function using hooks! Make sure your function is named according to the useXXXX scheme.

There is no function signature that you must follow in order for it to be hook - it can have whatever arguments and return type that you choose.

Learn more about custom hooks here