Graduate Program KB

r-- title: React Notes date: 2024-02-28 description: Matthew's React Notes slug: react


React Notes


Table of Contents

The Beginning

One of the core innovations of React was that they made the view a function of your application's State. This was often represented as v = f(s) or view = function(state). This allows us to worry only about the application state, and let React handle the rest. The other half of the core innovation came from encapsulating this idea into a proper component based API. To be able to properly embrace a component based API, React needed a way to allow a user to specify what the UI of a component should look like, but to do so from within the component itself. Typically, developers choose to separate their programs into distinct sections. This is especially true when using html, CSS and Javascript, they each have their own files. For React however, essentially combines these three elements, as they are all concerned with rendering the View.

This all lead to JSX. It allows you to write syntax similar to html, directly inside of Javascript. Here is an example:

function Profile({ user }) {
  return (
    <div className="profile">
      <Avatar username={user.avatar} />
      <Bio text={user.bio} />
      <ul>
        {user.preferences.map((preference) => (
          <li>
            <Setting
              key={preference.id}
              preference={preference}
            />
          </li>
        ))}
      </ul>
    </div>
  );
}

It combined the power and expressiveness of JS with the readability and accessibility of html. The beauty of it is that if you know html and Javascript, then JSX is very easy to pickup.

Initially React was mostly used for the creation of single-page applications. Now it is often treated more as a 'UI Primitive'. In that frameworks treat React as this UI Primitive, and then build additional features on top of it such as route pre-fetching, server side rendering and intelligent bundling.

Imperative vs Declarative Programming

React is declarative. But what is that? The definition of imperative vs declarative programming has commonly been found to be "Imperative programming is how you do something, and declarative programming is more like what you do". Let's review this distinction by looking at a real-life example. Say you are taking your partner our to dinner, and you arrive at McDonald's (fancy, I know). Imperative (how) would equate to: "I see that table near the playground is empty. My partner and I are going to walk over there and sit down". Declarative (what) would equate to: "Table for two please".

It is also important to note that any declarative approach typical;ly has some sort of imperative abstraction layer. With the McDonald's example, we assume that the waiter (does McDonald's have waiters?) knows all the imperative steps to get you to that table. Now back to programming, the lists below show what languages are imperative or declarative by nature.

Imperative

  • C
  • C++
  • Java

Declarative

  • html
  • SQL

Both

  • Javascript
  • C#
  • Python

An imperative example of a piece of Javascript is as follows

function add(arr) {
  let result = 0;
  for (let i = 0; i < arr.length; i++) {
    result += arr[i];
  }
  return result;
}

We write a function called add which takes in an array and returns the sum of each item in the array. add([1, 2, 3]) = 6. This is imperative as we are describing HOW to do something.

Now the declarative version of the same code:

function double(arr){
    arr.map((item) => item * 2);
}

In this example, given the same prompt as before, we have a function which outputs the same result, but does it in a different manner. We make used of the built in map function in Javascript. This can be related to our earlier example, where we we what we want to happen, but leave it to sort (our waiter), to figure out how it will occur. We also don't mutate any state, as it all occurs within map.

Below are some further definitions of both declarative and imperative programming to aid in understanding. Further Definitions

  • Declarative programming is: The act of programming in languages that conform to the mental model of the developer rather than the operational model of the machine.
  • Declarative programming is: Programming with declarations, i.e. declarative sentences.
  • Declarative languages contrast with imperative languages which specify explicit manipulation of the computer's internal state; or procedural languages which specify an explicit sequences of steps to follow.
  • The declarative property is where there can exist only one possible set of statements that can express each specific modular semantic. The imperative property is the dual, where semantics are inconsistent under composition and/or can be expressed with variations of sets of statements.
  • In computer science, declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow.
  • I draw the line between declarative and non-declarative at whether you can trace the code as it runs. Regex is 100% declarative, as it's untraceable while the pattern is being executed

Pure Functions

The main job of a developer is to maximise the predictability of your program. The root cause of any bug ever experienced, is because the expectations of the developer diverged form the reality of the program (the program was unpredictable). As a whole, we want to make our apps more predictable. This can be split into making the individual pieces which comprise our app more predictable. This boils down even further, to our functions being the individual pieces. Functions are simple. However, as programs grow, our functions can evolve into an unpredictable mess. Is there a set of rules we can use to keep our functions from becoming unpredictable over time? To make rules, we must first discover what makes a function unpredictable. It is believed that the answer to this question is side effects and inconsistent outputs.

Side Effects

Anytime a function does anything other than take some input, calculates some output, and returns a value (if this is the design of course), then we can say the function has a side effect. If we take a simple function:

function addTodo(todo){
    todos.push(todo);
}

It relies not only on the input it receives (todo) but also on todos. It also will create an observable change to the program by mutating todos. Therefore it has a side effect.

Side effects are not necessarily bad, usually they have the impact we wanted, however they are still unpredictable. One problem is because they are dependant on context outside of their local environments, they are contextual to the current state of the application. If the application states changes, they could behave differently.

function calculateFinalPrice(price, qty) {
  const total = price * qty
  return total * (1 + TAX_RATE)
}

This function has a variable TAX_RATE. This function will only work inside a program where this variable both exists, and is a decimal. If that variable were to be mutated or deleted, this would impact how calculateFinalPrice would execute, which by definition makes it unpredictable.

This is where our first rule comes in. No side effects.

Inconsistent Outputs

The next culprit in making functions unpredictable are inconsistent (or a lack of) outputs from a function. If a function has no return then it is unpredictable.

thisIsUnpredictable()

Either the function is doing nothing (unlikely otherwise it would not exist), or it is breaking our first rule of no side effects. Inconsistent outputs are also unpredictable and a behaviour that we do not want in our functions.

const friends = ['Ben', 'Lynn', 'Alex']
friend.splice(0, 1) // ["Ben"]
friend.splice(0, 1) // ["Lynn"]
friend.splice(0, 1) // ["Alex"]

There are a number of built-in methods which are also unpredictable (by design). These include random() or Date.now(). So now we have our second rule. Consistent Outputs.

The Benefits

Whilst writing functions to follow the rules, obviously they will become more predictable, but there are other benefits too. The functions become composable/reusable (as they do not rely on the context of the application). Due to functions returning the same output given the same input, functions become cacheable. Here is an example of caching for a function that checks if a number is prime or not:

let primeCache = {
  1: false,
}

const isPrime = (n) => {
  if (typeof primeCache[n] === 'boolean') {
    return primeCache[n]
  }

  for (let i = 2; i <= Math.sqrt(n); i++) {
    if (n % i === 0) {
      primeCache[n] = false
      return false
    }
  }

  primeCache[n] = true
  return true
}
export default isPrime
// Some other file -------------
import isPrime from './isPrime'

isPrime(997) // true
isPrime(997) // true (from cache)
isPrime(997) // true (from cache)

Functions also become testable (or at least optimized for testing). They also become far more readable. The inputs and outputs are explicit, and there are no surprises.

These rules are essentially the re-incarnation of the functional programming principle of Pure Functions. A function is considered pure if it contains no side effects, and if given the same input, will always return the same output. By striving to keep functions pure, the predictability of your program will be increasing whilst minimising the amount of bugs it may have.

Components

The primary benefit of HTML is that it allows us to create rich, structured markup in a declarative way. The primary benefit of Javascript is that it gives us a set of tools for managing the complexity of building an application. React is the 'lovechild' of the two, giving us declarative-ness of HTML but with the feature set of Javascript.

A react component is essentially just a function. The same mental model we have about Javascript functions can be applied to React components, because they are essentially the same. The main change we need to make, is to capitalise the first letter of our component. This is so that React reads it as a React component, rather than a normal DOM element. (i.e. Footer vs footer). Say we have a file Authors.jsx:

export default function Authors() {
  return (
  <aside>
    <h2>Authors</h2>
    <ul>
      <li>Tyler McGinnis</l1>
      <li>Ben Adam</l1>
      <li>Lynn Fisher</l1>
    </ul>
  </aside>
  )
}

And a second file App.jsx

import Authors from './Authors'

export default function About() {
  return (
    <main>
      <h1> About Us</h1>
      <p>We write JavaScript and words about Javascript</p>
      <Authors />
    </main>
  )
}

Here we are creating a component in the Authors.jsx file called 'Authors'. Then in our App.jsx file, we import this component we made, and place it directly after the p. This will place that entire component in that spot. As the browser paints the UI to the screen, it could be said that our App.jsx file read as if the Author tag did not exist, but instead had the raw HTML from the component.

Obviously if we start creating many components, and their own file for each, our imports will start to look a little ridiculous. Ideally, if we have a component that needs to be reused somewhere else, we can place it in it's own file, else we can create the component in whatever file it is that requires said component.

JSX

The most accurate description that can be given of a react component is: "A function that returns a description of the UI". JSX is the magic behind having HTML-ish looking syntax within our JavaScript files. For the most part, writing JSX is pretty similar to HTML minus a few things to be aware of.

When writing React components, we must ensure not to return adjacent elements (it can only return one top-level element). Consider below:

import * as React from 'react'

export default function Authors(){
  return (
    // <>
    <h2>Authors</h2>
    <ul>
      <li>Tyler McGinnis</li>
      <li>Ben Adam</li>
      <li>Alex Brown</li>
    </ul>
    // </>
  )
}

The issue is that it tries to return both the h2 tag and the ul tag. We could wrap this whole component in a div, but this may cause semantic issues. Instead, we use React.Fragment. Additionally, we could use and which works in the exact same way. I put them in the code above but commented them out so we can see where they should go.

Due to JSX being converted to Javascript at some point in execution, we need to ensure we don't use any reserved keywords. One of these being class. So when we have an img tag for example, we would define 'className', rather than just the usual 'class'.

We also can't have variable names which contain a '-'. We need to convert any normal HTML elements that have this into the camelCase form. For example: stroke-width in HTML would become strokeWidth in JSX.

Whenever we want to use an expression (something that produce a value), we need to wrap it in curly braces. See below for an example of this:

function Welcome {
  const name = 'Tyler';

return (
  <>
    <h1> Hello, {name}</h1>
    <p>
      Today is {new Date().toLocalDateString()}
    </p>
    <p>  What is 2 + 2? {2 + 2}</p>
  </>
)
}

Conditional Rendering

Sometimes we want to render one of a selection of options based on a piece of state. For example, we may want to show a welcome back message to a user who is authorized, or a piece of text telling a user to login to be able to complete something.

function Dashboard() {
  const authed = isAuthed()

  if (authed === true) {
    return <h1> Welcome back! </h1>
  } else {
    return <h1> Login to see your dashboard </h1>
  }
}

If we had further conditions, we could simply add an else if case, and more as needed. Using JavaScript's ternary operator, we can simplify the above code (typically this is only used for rendering UI with a single condition). Combining our knowledge of expressions being wrapped in curly braces, and the ability to use a ternary operator to render different UI, we get the code below:

//prettier-ignore
function Dashboard(){
  return (
    <>
      <Logo />
      {isAuthed() === true
      ? <h1> Welcome back! </h1>
      : <h1> Login to see your dashboard </h1>}
    </>
  )
}

We could also return null if we want React to render nothing (perhaps whilst it is still loading or waiting for some data).

Creating lists is a fundamental part of app development. Most frameworks already come with a special API to accomplish this task as it simply comes up so often. In React, we can use the map method (taken from JS), to create a new array of li's (a list!).

import tweets from './tweets'

export default function Tweets() {
  return (
    <>
      <ul id="tweets">
        {tweets.map((tweet) => (
          <li>{tweet.text}</li>
        ))}
      </ul>
    </>
  )
}

However, whenever we use .map to create a list in React, we need to add a unique key prop to each list item. We can just change one line from our previous example.

<li>{tweet.text}</li>
// Instead becomes
<li key={tweet.id}>{tweet.text}</li>

When we give each item a unique key prop, it helps React know which items, if any to change throughout different renders of that components, this will help React render the lists as fast as possible.

Props

When you have a system reliant upon composition, it's critical each piece of the system has an interface for accepting data from outside of itself. A clear example is functions. Many functions would not work without having an interface for accepting data (in this case arguments represent this interface). Similarly, React relies heavily on composition, there needs to exist a way to pass data into components. This is where props comes in.

Props are to components what arguments are to functions. There are two parts to understanding how props work.

  1. How to pass data into components
  2. Accessing the data once it's been passed in

Passing Data to a Component

You pass data to a React component the same way you'd set an attribute on a HTML element.

<img src='' />
<Hello name='Matt' />

In this example, we pass a name prop to the Hello component. We can pass other things than strings too, including functions or other values:

<Profile
  username="Mattzuf"
  authed={true}
  logout={handleLogout}
  header={<h1>👋</h1>}
/>

Notice above the curly braces when passing items (but we didn't use it every time). Whenever we pass a prop which is not a string, it must be wrapped inside of a curly brace. We could even parse object literals as props, but this time we would use two sets of curly braces.

<DatePicker
  settings={{
    format: 'yyyy-mm-dd',
    animate: false,
    days: 'abbreviate',
  }}
/>

Accessing Props

Since components are just functions, React will put all of the props on an object, and pass that object as the first argument to the function. See below:

function Hello(props) {
  return (
    <h1>
      Hello, {props.first} {props.last} {/* Hello, Matthew Mandzufas*/}
    </h1>
  )
}

export default function App() {
  return <Hello first="Matthew" last="Mandzufas" />
}

We can also destructure the object if we like. The above example becomes like below. Not within the Hello() function, now we have curly braces where the props/arguments are.

function Hello({ first, last }) {
  return (
    <h1>
      Hello, {first} {last}
    </h1>
  )
}

Additionally we can access whatever data is between an opening and closing tag of an element. We do this using props.children.

function Header(props) {
  return <h1 className="header">Some text</h1>
}

function Layout(props) {
  return (
    <div className="layout">
      <Sidebar />
      {props.children}
      <Footer />
    </div>
  )
}

You can think of a component with a children prop as having a placeholder that can be filled by its parent component. Because of this, its common to utilize children to create Layout type components which encapsulate styling/logic, but leave the content to the consumer of the component.

An example may be a modal component. It is styled, and has defined logic, but we can re-use this component, and each time have different text between the opening/closing tags, and therefore provide the modal with different content.

Elements vs Components

How do we go from something like <Icon />, to Javascript that the browser can understand? The answer likes in React elements. A React element is an object representation of a DOM node. An example of this looks like below:

{
  type: "h1",
  props: {
    children: "Profile",
    className: "header"
  }
}

This is the object representation of a DOM node that looks like this:

<h1 class="header">Profile</h1>

React keeps references to all of the elements in our application. If a reference changes, the React knows exactly where in the DOM it needs to update. To be able to create a React element, we have a dew options. One of them being the jsx-runtime package.

import { jsx } from 'react/jsx-runtime'

const element = jsx('h1', {
  className: 'header',
  children: 'Profile',
})

jsx takes in two arguments here. The first being a tag name string (like div or span etc.). The second argument is an oject of attributes we want an element to have. In this example, we have a className of "header" and text (children) of "Profile".

Not only can we create React elements by passing native HTML elements, but we can also pass in other React components as the first argument to jsx as well. So what is the difference between writing JSX and using the jsx transform. According to the browser, there is no difference. However writing UI descriptions is far easier with JSX. So now we know to correctly classify something like <Icon /> as creating an element.

Handling Events

To build an interactive and modern application, we need to be able to listen to and respond to events. The API to do this in React is fairly simple, and it involves both props and functions. Say we have a button, that when clicked should alert the user with the text 'ouch'.

export default function AlertButton() {
  const handleClick = () => alert('OUCH')

  return <button onClick={handleClick}>AlertButton</button>
}

Whenever button is clicked, it will invoke handleClick. Typically, it is a good idea to encapsulate the event handler into its own function, and use handle as the suffix in addition to the name of the event. Notice that we pass our event handler as a reference rather than a invocation (no () at the end). If we passed it as an invocation, as soon as the Javascript engine came across it, the function would be invoked.

We also tend to create the event handler inside the component rather than outside. If we decided to use whatever is passed in as a message prop to our button instead, it allows us to have automatic access to the prop. This makes the reusability far greater.

Whenever an event handler is called by React, React will pass an object as the first argument to the event handler which contains all the information about the triggered event. An example taken from the course may look something like below:

SyntheticBaseEvent {
  bubbles: true,
  cancelable: true,
  clipboardData: DataTransfer {},
  currentTarget: null,
  defaultPrevented: false,
  eventPhase: 3,
  target: li,
   timeStamp: 52607.59999999404,
  type: "copy",
}

Whilst there is a lot of data not useful to use, dependant on the context, some properties would be vital to the success of the function.

Preserving Values With UseState

React in it's purest form, is a library for building user interfaces. It's so simple that the formula we covered previously describes it very well. View = function(state). We have covered how to create view with jsx and encapsulating that view within a function to get a component. Now we will talk about state. Say we have a simple counter app which keeps track of how many times a user has clicked a button.

export default function Counter() {
  let count = 0;

  cont handleClick = () => count++;

  return (
    <button onClick>={handleClick}>
      {count}
    </button>
  )
}

However this example would not work. Like javascript, each invocation is its own isolated environment, with its own scope and context. We need a way to tell React to preserve the value of count between different invocations. Luckily, React has a hook which does exactly that. It is called useState. React hooks can be thought of a special function which React provides as help.

useState

This is the best way to preserve a value across component renders (invocations). It comes built-in with React. It takes in a single argument as the initial value for that piece of state, and returns an array with the first item being the state value, and the second being a way to update that state. More recently, it can be done like below:

const [count, setCount] = useState(0)

Now that we know of useState(), we can use it to fix our counter app from before so that it works.

export default function Counter() {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)

  return <button onClick={handleClick}>{count}</button>
}

Now our value of count will persist across all renders, no matter how many times it is rendered.

Using useState

We are going to explore a more advanced app which employs useState. It will walk through the following state progression:

  1. We'll start with a single piece of state
  2. We'll add multiple pieces of state to the component
  3. We'll update our state to see how we can manage state when it's an object
  4. We'll update our state to see how we can manage state when it's an array of objects

Combining all steps, we end up with the following Todo app:

import * as React from 'react'

export default function Todo() {
  const [todo, setTodo] = React.useState({
    // Represent todo item as an object instead of individual values
    id: 1,
    label: 'Learn React',
    completed: false,
  })
  const [completed, setCompleted] = React.useState(false)
  const [editing, setEditing] = React.useState(false)
  const handleCheckboxClick = () =>
    setTodo({
      ...todo,
      completed: !todo.completed,
    })

  const handleEditClick = () => setEditing(!editing)

  const handleUpdateLabel = (e) =>
    setTodo({
      ...todo,
      label: e.target.value,
    })

  return (
    <div>
      <label htmlFor="checkbox">
        <div>
          <input
            type="checkbox"
            id="checkbox"
            checked={todo.completed}
            onChange={handleCheckboxClick}
          />
          <span />
        </div>
        {editing === true ? (
          <input type="text" value={todo.label} onChange={handleUpdateLabel} />
        ) : (
          <span>{todo.label}</span>
        )}
      </label>
      <button onClick={handleEditClick}>{editing ? 'Save' : 'Edit'}</button>
    </div>
  )
}

Updating Object State

It is important to note that passing a value to useState's updater function will always replace the current piece of state. This means that if we have a piece of state which is an object (like we have in our todo example), it won't be merged with the current state.

const [user, setUser] = React.useState({
  id: 1,
  name: 'Tyler McGinnis',
  email: 'tyler@ux.dev',
})

const handleUpdateEmailIncorrectly = (email) => {
  /*
    This will overwrite our user state
    with { email }.
  */
  setUser({ email })
}

We instead want to merge the new value with the existing state object. We use javascript's spread operator to achieve this. We spread them onto a new object, then pass that object to the updater function.

const handleUpdatEmail = (email) => {
  const updatedUser = {
    ...user,
    email: 'tyler@ui.dev',
  }
  setUser(updatedUser)
}

At the moment in our example, we are only render a single Todo element, but we obviously want more (one for everyone the user creates). This brings up a problem with the current structure of our app. Currently the state of each todo item is coupled to our todo component. Instead of each todo component managing it's own state, we could lift the state up to the parent TodoList component. TodoList would then own (and manage) the state of each todo item, and then we could pass data down to components that need it via props. To update the state for each individual todo component, from the TodoList, we need an updater function. This function will live where the state is, and via props, invoke the function from the child components, where the event handlers live. Before we learn how to implement that functionality, lets first learn about updating array state.

Updating Array State

This is very similar to objects in that when we want to update an array that on state, we will need to pass the updater function a new array which will replace the current array. We could add an element, remove an element or update some elements. The point being that we do not modify the original state array, but instead a new array.

Completed Todo App

Now get ready for a large code example. First we have TodoList.jsx

import * as React from 'react'
import Todo from './Todo'
import TodoComposer from './TodoComposer'

export default function TodoList() {
  const [todos, setTodos] = React.useState([
    { id: 1, label: 'Learn React', completed: false },
    { id: 2, label: 'Learn Next.js', completed: false },
    { id: 3, label: 'Learn React Query', completed: false },
  ])

  const handleUpdateTodo = (updatedTodo) => {
    const newTodos = todos.map((todo) =>
      todo.id === updatedTodo.id ? updatedTodo : todo,
    )
    setTodos(newTodos)
  }

  const handleDeleteTodo = (id) => {
    const newTodos = todos.filter((todo) => todo.id !== id)
    setTodos(newTodos)
  }

  const handleAddTodo = (newTodo) => {
    const newTodos = [...todos, newTodo]
    setTodos(newTodos)
  }

  return (
    <ul>
      <TodoComposer handleAddTodo={handleAddTodo} />
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          todo={todo}
          handleUpdateTodo={handleUpdateTodo}
          handleDeleteTodo={handleDeleteTodo}
        />
      ))}
    </ul>
  )
}

Then Todo.jsx

import * as React from 'react'

export default function Todo({ todo, handleUpdateTodo, handleDeleteTodo }) {
  const [completed, setCompleted] = React.useState(false)
  const [editing, setEditing] = React.useState(false)

  const handleCheckboxClick = () =>
    handleUpdateTodo({
      ...todo,
      completed: !todo.completed,
    })

  const handleEditClick = () => setEditing(!editing)

  const handleEditTodo = (e) =>
    handleUpdateTodo({
      ...todo,
      label: e.target.value,
    })

  const handleDeleteClick = () => handleDeleteTodo(todo.id)

  return (
    <li>
      <label htmlFor={todo.id}>
        <div>
          <input
            type="checkbox"
            id={todo.id}
            checked={todo.completed}
            onChange={handleCheckboxClick}
          />
          <span />
        </div>
        {editing === true ? (
          <input type="text" value={todo.label} onChange={handleEditTodo} />
        ) : (
          <span>{todo.label}</span>
        )}
      </label>
      <div>
        <button onClick={handleEditClick}>{editing ? 'Save' : 'Edit'}</button>
        {!editing && <button onClick={handleDeleteClick}>Delete</button>}
      </div>
    </li>
  )
}

And finally TodoComposer.jsx

import * as React from 'react'

function createTodo(label) {
  return {
    id: Math.floor(Math.random() * 10000),
    label,
    completed: false,
  }
}

export default function TodoComposer({ handleAddTodo }) {
  const [label, setLabel] = React.useState('')

  const handleUpdateLabel = (e) => setLabel(e.target.value)

  const handleAddTodoClick = () => {
    const todo = createTodo(label)
    handleAddTodo(todo)
    setLabel('')
  }

  return (
    <li>
      <input
        placeholder="Add a new todo"
        type="text"
        value={label}
        onChange={handleUpdateLabel}
      />
      <button disabled={label.length === 0} onClick={handleAddTodoClick}>
        Add Todo
      </button>
    </li>
  )
}

Why React Renders

Given our equation from before of view = function(state) or v = f(s), it makes people ask the question exactly when and how is f invoked. Or when and how does React update the view? Let's first go one step back to first principles.

What is Rendering?

Simply: "Rendering is just a fancy way of saying that React calls your component with the intent of eventually updating the view". Two things happen when React renders a component. First React creates a snapshot of your component which captures everything React needs to update the view at that particular moment in time (props, state, event handles and a description of the UI). From there, React will take a description of the UI and uses it to update the view.

In order to get the starting UI of your application, React will do an initial render, starting at the root of your application. To create a root, we first grab the HTML element we want to mount our React app to, then we pass it to the createRoot function. From here we can call root.render, passing it a React element which will serve as the start point for the UI of our application. Typically this is done as below, and will automatically be set-up by running create-react-app.

import { createRoot } from 'react-dom/client'
import App from './App'

const rootElement = document.getElementById('root')
const root = createRoot(rootElement)

root.render(<App />)

This is only our initial render however, subsequent renders occur when we change the State in our application (remember our statement before of v=f(s)). The only thing that will trigger a re-render is a change of state.

As an event handler is invoked, it has access to props and state in the moment the snapshot was created. From here, if useState's updater function is used, it will re-render the component and update the view.

Batching

React uses an algorithm called batching to calculate the new state. Whenever React encounters multiple invocations of the same updater function, it will keep track of them, but only the result of the last invocation will be used for the new state.

We also find out that React will only re-render once per event handler, even if the Handler contains updater for multiple pieces of state. However, whenever state-changes, React will re-render the component which owns the state along with ALL of its child components. If we do not want this behaviour to occur, we can use React.memo. This is a function which takes a React component as an argument, and returns a new component which will only re-render if its props change. We create our function like normal, and export it as such:

export default React.memo(myFunction)

Strict Mode

When we enable strict mode in our React apps, everything is re-rendered an extra time. This is to ensure our components are pure, and it will quickly become obvious if they are not during the second render. We wrap our app in StrictMode at the highest level to enable this functionality.

Reality Check

In the real world, it is quite common to complete tasks outside of React such as fetching data from a server, interacting with the DOM or using native browser API's. Obviously it needs to be able to handle these outside cases, and to do so in a way which does not destroy the simplicity of the mental model.

Managing Effects

Obviously we have our formula of v=f(s), but we need the function to be pure, in that it runs without any side-effects.

Rule #0 When a component renders, it should do so without running into any side effects.

If we remember back to our React app which displayed a different greeting each time a button were pressed. We may want to add additional features to this app. One of these being to remember the users last selection, and persist this in the browser, so the next time a user loads the page, their choice is remembered.

localStore.setItem('index', index)

If we set this in the middle of our component, our desired effect will be achieved, but it does so by violating Rule #0 which we just defined. It is a side effect. So how can we fix this?

Rule #1 If a side effect is triggered by an event, put that side effect in an event handler.

The entire point of an event handler is to encapsulate the logic for an event. That means for our previous example, that we would move our side effect into the event handler (in this case handleClick()). Now our side effect is still part of the component, however it is abstracted to a part which is not involved with rendering, and therefore no longer violates Rule #0. But we don't yet have a way to set our initial index to what we have in the local storage.

const [index, setIndex] = React.useState(() => {
  return Number(localStorage.getItem('index'))
})

Rule #2 If a side effect is synchronizing your component with some external system, put that side effect inside useEffect. The useEffect hook lets us run a side effect which synchronises our component with some outside system. It works by removing the side effect from React's normal rendering flow, and waiting till after the component has rendered. By default out effect gets invoked after every re-render. useEffect however does accept a second argument which gives us more control. We provide React with all the dependencies that effect needs to run, like so:

React.useEffect(() => {
  document.title = `Welcome, ${name}`
}, [name])

Now whenever name is changes, React will re-run the effect. Given an empty array (indicating that the effect doesn't depend on any values), then the effect will only be run after the initial render.

Managing Effects Part 2

All of our examples till now have been side effects local to the users device. Network requests however are a very common side effect. The course has provided an example of how to properly update state without side effects using a pokemon api to display data.

import * as React from 'react'
import { fetchPokemon } from './api'

export default function App() {
  const [id, setId] = React.useState(1)
  const [pokemon, setPokemon] = React.useState(null)
  const [loading, setLoading] = React.useState(true)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    const handleFetchPokemon = async () => {
      setLoading(true)
      setError(null)
      const { error, response } = await fetchPokemon(id)
      if (error) {
        setError(error.message)
      } else {
        setPokemon(response)
      }
      setLoading(false)
    }
    handleFetchPokemon()
  }, [id])

  return <main>{JSON.stringify({ id, pokemon }, null, 2)}</main>
}

Things to note:

  • We have set a loading and error state to keep track of these states
  • We reset the values of these states before each effect
  • At the moment we just dump the data from the api, obviously this would be formatted/presented nicely for a real application
  • This code possess an issue, we don't know how long the request will take, and id may be updated again before the previous request and subsequent re-render completes, and our end UI result will be whatever was resolved last

To solve this last dot point, React provides a way to cleanup after each useEffect. If we return a function from our effect, React calls that function before it would call useEffect again, then one final time when the component is removed from the DOM. So to fix our pokemon problem, we can return a function from useEffect such as this:

// Updated useEffect
React.useEffect(() => {
  let ignore = false
  const handleFetchPokemon = async () => {
    setLoading(true)
    setError(null)
    const { error, response } = await fetchPokemon(id)
    if (ignore) {
      return
    } else if (error) {
      setError(error.message)
    } else {
      setPokemon(response)
    }
    setLoading(false)
  }
  handleFetchPokemon()
  return () => {
    ignore = true
  }
}, [id])
  • When the effect runs, the ignore variable is initialised and set to false
  • When the cleanup function runs (which occurs when the effect is stale), ignore is set to true
  • Then in the next iteration, ignore would be true and therefore would not be run

If we were trying to increment some count state, we could do setCount(count + 1), or we could setCount((c) => c + 1). In the second version, we don't rely on closure, so if we were updating within a useEffect hook, we no longer would need the count in the dependency array.

Strict Mode

When strict mode is enabled, React re-renders an extra time, re-runs effects an extra time, and checks for deprecated APIs. This could be considered a stress test for our rules, if our functions are impure, then we will find out quickly.

Preserving Values with useRef

When React re-renders, we lost the value of all our regular Javascript variables. What if we wanted these to persist across renders? This sounds like something that useState() could be used for. However, in the course example provided, this would be introducing a state which has nothing to do with our view. This breaks our v = f(s) formula. We want something to preserve our value across renders, but it has nothing to do with the view, so React need not re-render on value change. This is where useRef comes in. It creates a value that is preserved across renders, but won't trigger a re-render when it changes.

When you call useRed, you get back a ref. This is an object with a mutable, current, property. An initial value can also be specified.

import * as React from 'React'

export default function App() {
  const ref = React.useRef(null)
  console.log(ref.current) // null
  ref.current = 2412
  console.log(ref.current) // 2412
  ref.current = { foo: 'bar' }
  console.log(ref.current) // { "foo": "bar" }
}

The course goes onto to talk about we can utilise useRef as a ref on div elements so we know where to automatically scroll to on buttonClick.

Teleportation with Context

There is a linear relationship between the size of your application and how difficult it is to share state across that application. This is true when you have an app that is collection of components. What we've been taught thus far is that if we have state which multiple components depend on, we wish to lift the state to the nearest parent component and then pass it back down via props.

Sometimes, in complex applications this can become overly redundant or completely unmanageable. Even some libraries need the ability to pass data to any component in the tree, regardless of how nested it is. React's built-in api context, can help to solve this. It allows data to be 'teleported' anywhere in the component tree without the need for props.

Three things we need to know:

  1. Creating the 'teleporter' context
  2. Deciding what we want to teleport and to where
  3. Getting what we teleported from inside a component

1.

import * as React from 'react'
const delorean = React.createContext()
export default delorean

2. Context gives granular control over which parts of the component tree have access to our 'teleported' data. When we create a new Context as shown above, React gives an object with the provider property. This property accepts a value prop, which is the data we want to teleport to a component in the provider's subtree.

import delorean from './delorean'
// ...

const marty = {
  name: 'Marty McFly',
  age: 17,
}

return <delorean.Provider value={marty}>// ...</delorean.Provider>

Now everything wrapped inside our Provider will have access to the value marty.

3. To gain access to the Providers value, we utilise useContext();

import delorean from './delorean'
// ...
const marty = React.useContext(delorean)
// ...
<li>{marty.name}: Age {marty.age}</li>

Obviously in this example, marty was a static object so we could have simply exported it, however we can also teleport a piece of state like this. React still re-renders in the same way it normally would when a piece of state updates. If the state is at the root of our application, the entire tree will be re-rendered. We can also set a default value with createContext for our Context, in case our App does not have an associated Provider up in the tree.

Complex State with useReducer

A fundamental array method in JavaScript is forEach. It allows you to invoke a function for each element in an array. This is not always viable, and sometimes we should use the Reducer Pattern instead. The key difference between reduce and forEach is that reduce can keep track of accumulated state internally without additional help.

The reducer pattern allows a collection as input, and outputs a single value. The reducer function is invoked for each element in the collection. What if instead of the collection being an array, it were a collection of user actions which happened over time, with each new action invoking the reducer to get the new state of the application.

Being such a useful pattern, React comes with the built-in hook called useReducer(). useReducer not only returns the state, but also a way to update that state.

const [state, dispatch] = React.useReducer(reducer, initialState)

Using this, we can see a simple demonstration of a button that when clicked, increments the count above it. We have seen different iterations of this before in the past, but this one utilises useReducer.

import * as React from 'react'

function reducer(state, value) {
  const nextState = state + value

  console.log(
    `Reducer invoked. State: ${state}, Value: ${value}, nextState: ${nextState}`,
  )
  return nextState
}
const initialState = 0

export default function Counter() {
  const [count, dispatch] = React.useReducer(reducer, initialState)
  const handleIncrement = () => {
    dispatch(1)
  }
  return (
    <main>
      <h1>{count}</h1>
      <button onClick={handleIncrement}>+</button>
    </main>
  )
}

When invoked, what is passed to dispatch will be passed as the second argument to the reducer function (in this case, value). The first argument (state) is implicitly passed by React. To handle a decrement with the above example, it would be as simply as creating a handleDecrement function, which passes -1 to dispatch.

This current setup would make it hard to implement something such as resetting the count, as we can't just pass 0 to dispatch. Instead we might remodel our reducer function, to check if the action (rather than value), passed in is equal to 'increment', 'decrement' or 'reset'. Obviously we would now be calling dispatch with these string instead.

function reducer(state, action) {
  if (action === 'increment') {
    return state + 1
  } else if (action === 'decrement') {
    return state - 1
  } else if (action === 'reset') {
    return 0
  } else {
    throw new Error('This type of action is not supported')
  }
}

Perhaps we want to introduce the same functionality but with a slider which determines how much to increment/decrement by. We would first create a slider which took in three props, min, max and change.

<Slider min={1} max={10} onChange={handleUpdateStep} />

We would also need to change our initialState from being a static 0. We refactor it to become an object. Doing ths, we would also need to update our reducer to account for this.

const initialState = { count: 0, step: 1 }

function reducer(state, action) {
  if (action.type === 'increment') {
    return {
      count: state.count + state.step,
      step: state.step,
    }
  } else if (action.type === 'decrement') {
    return {
      count: state.count - state.step,
      step: state.step,
    }
  } else if (action.type === 'reset') {
    return {
      count: 0,
      step: state.step,
    }
  } else if (action.type === 'updateStep') {
    return {
      count: state.count,
      step: action.step,
    }
  } else {
    throw new Error("This action type isn't supported.")
  }
}

Notice that here we have changed action to action.type in all our if statements, we changed +/- 1 to +/- state.step and finally added the 'updateStep' action type.

The final step is the eventHandler for the slider. Our other eventHandlers should also be updated in a like manner to convert them to objects.

const handleUpdateStep = (step) => {
  type: 'updateStep', step
}

This example shows how simple it is to update one piece of state, based on another piece of state. Here we update count based on the value of step at any given moment.

Like useState, useReducer allows you to add state to a component that will be preserved across renders, and trigger a re-render when changed. However it also allows us to manage that state using the reducer pattern. It offers more flexibility as we can decouple how state is updated from the action which triggered it. This is typically more declarative. If state updates independently, useState should be fine, it state is based on another piece of state, utilise useReducer.

Referential Equality and Why It Matters

When we create a variable in Javascript, the variable can store either primitive data or a reference value. If the value is number, string, boolean etc, then it is primitive, else it is a reference value (like const myArr = []). Looking at the in-memory value of a primitive gives you the actual value, but for a reference, theres a reference to a spot in memory. This is why that when primitives are copied from another primitive, affecting the original copy will not affect the copied version, whereas with reference values, affecting the original will affect the copy, as they reference each other.

As we know, React re-renders components and all their children when a change in state occurs. We could opt-out of this behaviour for 'expensive' components, and choose to only re-render when it's props change. We could use React.memo which is a higher-order component.

From our earlier greeting example:

// Wave.jsx
import * as React from 'react'

function Wave() {
  console.count('Rendering Wave')
  return (
    <span role="img" aria-label="hand waving">
      👋
    </span>
  )
}

export default React.memo(Wave)
  • Regardless of how many times we click button (in app.jsx), Wave will only render once
  • It has no props so cannot be re-rendered
  • If we decided to pass in options (like changing the emoji type), we may run into some issues
    • Wave.jsx will be re-rendered due to referential equality
    • This is because it simply uses === to determine if props have changed
    • The reference values are compared by memory location, an we pass a new object every render of app.jsx, therefore changing memory location

To fix our problems we can use the built-in useMemo component. It allows us to cache the result of a calculation between renders.

const cachedValue = React.useMemo(calculateValue, dependencies)
  • First argument is a function which returns the value to be cached
  • Second argument similar to useEffect. An array of dependencies the function depends on, if changed, the cached value will be recalculated

For example with our options object for our wave example:

const options = React.useMemo(() => {
  return {
    animate: true,
    tone: waveIndex,
  }
}, [waveIndex])

Unless waveIndex changes, our options object will be referentially identically across renders, meaning our Wave.jsx file will only be render once initially, and then subsequently when waveIndex is updated.

We can also use useCallback instead of useMemo when we are caching a function. This caches the function itself rather than just the result of calling a function.

Managing Advanced Effects

The three rules we defined earlier in the course cover about 95% of all use cases. However there are still some edge cases. For example, we might have an application which displays the current height and width of a window. We could implement this with useEffect perhaps, so that the application re-renders each time the state (height/width) is changed. However there will always be a slight delay, as useEffect runs asynchronously after render. This is one use case where we should prefer useLayoutEffect. The code inside this hook wil be processed before the browser repaints the screen. This forms rule #3 (Now we have 0, 1, and 2). useLayoutEffect and useEffect are identical in terms of API layout.

Rule #3

  • If a side effect is synchronising your component with some outside system and that side effect needs to run before the browser paints the screen, put that side effect inside useLayoutEffect.

A second example that falls within the 5% is displaying a users online connectivity (if they are connected to the internet). To be able to do this, we subscribe to the browsers navigator.onLine state. However we must create another piece of state to handle changes which is redundant.

We must recognise that a piece of non-React-state managed by some outside system, can be managed using the useSyncExternalStore hook to subscribe a component to that state.

How useSyncExternalStore works:

export default function App() {
  const [networkStatus, setNetworkStatus] = React.useState('online')

  React.useEffect(() => {
    const handleChange = () => {
      setNetworkStatus(navigator.onLine ? 'online' : 'offline')
    }

    window.addEventListener('online', handleChange)
    window.addEventListener('offline', handleChange)

    return () => {
      window.removeEventListener('online', handleChange)
      window.removeEventListener('offline', handleChange)
    }
  }, [])

  return (
    <div>
      <span className={networkStatus} />
      <label>{networkStatus}</label>
    </div>
  )
}

Our fifth and final rule is created:

Rule #4

  • If a side effect is subscribing to an external store, use the useSyncExternal hook.

However we must be wary of referential equality. We may need to extract the functionality to outside of the main component, so that React does not think it is a new function every time, and therefore un-subscribe then re-subscribe each time.

Abstracting Reactive Values with useEffectiveEvent

The whole purpose of useEffect is to encapsulate a side effect which synchronises with our component with some outside system. It is invoked once after the initial render, and then once each time something in its dependency array is updated.

Sometimes we may want to be able to use a reactive value inside of useEffect, but have it do nothing with regards to synchronising the component. useEffectEvent sort of allows us to do this. We can use it as a way to abstract those values (reactive, non-synchronising) into their own event handler to then be used inside useEffect (without the need of putting them in the dependency array).

const onPageView = React.useEffectEvent((url) => {
  pageview(url, state)
})

React.useEffect(() => {
  onPageView(url)
}, [url])
  • useEffectEvent allows us to abstract react but non-synchronising values out of useEffect
  • url is still included in dependency array, and it's passed as an argument to onPageView

Creating Custom Hooks

A recap of the hooks we have covered so far:

  • useState
    • Create a value that is preserved across renders and triggers a re-render when it changes
  • useEffect
    • Synchronise a component with some external system
  • useRef
    • Create a value that is preserved across renders, but won't trigger a re-render when it changes
  • useContext
    • Get access to what was passed to a Context's Provider
  • useReducer
    • Create a value that is preserved across renders and triggers a re-render when it changes, using the reducer pattern
  • useMemo
    • Cache the result of a calculation between renders
  • useCallback
    • Cache a function between renders
  • useLayoutEffect
    • Synchronise a component with some external system, before the browser paints the screen
  • useSyncExternalStore
    • Subscribe to an external store
  • useEffectEvent
    • Encapsulate a side effect that synchronises your component with some outside system

Obviously these hooks have many use cases within React applications. Using our earlier example of useSyncExternalStore for checking and displaying a users online connectivity, we recognise we may want to reuse this functionality elsewhere in the application. We can use React custom hooks (rather than just copy pasting) to achieve this in a way where our non-visual logic is not coupled directly to visual components.

All the functionality can be abstracted to another file, then simply imported into all files which require that custom hook.

// App.jsx
import useNetworkStatus from './useNetworkStatus'
const networkStatus = useNetworkStatus()

// useNetworkStatus.js
const getSnapshot = () => {
  return navigator.onLine ? 'online' : 'offline'
}

const subscribe = (callback) => {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)

  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

export default function useNetworkStatus() {
  return React.useSyncExternalStore(subscribe, getSnapshot)
}

Note that our hook should start with 'use' as per every other hook we have covered so far.

Rebuilding useHooks