A Crash Course in React

Getting Started with the React Library

James Collerton
10 min readMay 2, 2023

Audience

This article is aimed at developers with a solid understanding of JavaScript and general web programming, who are looking to expand into React. It isn’t designed as a tutorial, more as a ten minute overview of what using it may look like.

It may also be helpful to understand TypeScript, which you can pick up from my article here.

Argument

Initially, what is React? React is a library (not a framework) for helping developers build user interfaces. You build a UI by combining components. For example, you may have a component for a button or a text box. We’ll clarify this concept with examples later.

There are frameworks that sit around React, the one that we will be using is Next.js. However, we won’t cover it in detail here.

One of the benefits of having React as a library is that it can be incrementally adopted into your projects, you don’t need to change everything all at once! You can bring in functionality as and when you need it.

OK, let’s get stuck in.

Kicking off a React Project

You can kick off a React project in a number of ways. However, we’ll be using Next.js and the command npx create-next-app.

Creating a React app with Next.js

From there we can go into the created directory, run npm run dev, visit http://localhost:3000, and get a nice welcome screen!

We will ignore the majority of what is generated, instead focussing on the src/pages/index.tsx file. The accompanying files are from the Next.js framework, which we only need to run our React code.

Components

The first things we will talk about are components. These are the building blocks of your web page. Strip your index page down to the below.

<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>

</main>
</>

Run the application and make sure you get a blank screen. We’re ready to write our first component!

To do so, create a new file in src/components called List and pop in the below.

import React from 'react';

// Here we create our list component, notice
// how it is a function.
const List = () => {
return (
<>
{/* We return some JSX code, which is
then translated into browser-appropriate
languages */}
<h1>Our list goes here!</h1>
</>
);
};

// Finally we export our component for use in
// our page.
export default List;

We can then put it into our index page.

<main className={styles.main}>
<List></List>
</main>

And on running the application we will receive this screen.

A successfully rendered component.

OK, that was easy enough! However, let’s dig behind the scenes and work out what’s happening.

The first thing is JSX. This is a syntax extension for JavaScript that lets you write HTML-like code in JavaScript. It’s important to note this is separate to React, and although used in tandem, it’s possible to employ them separately.

The next thing is the trigger-render-commit cycle.

There are two types of trigger:

  1. Initial page load.
  2. Something changing, prompting a component reload.

Once we have received a trigger, React calls your components to decide what to display on screen. In the initial page load it will render everything, but in component reloading it will calculate what has changed, and what has not.

Finally we want to commit our change. When React has carried out its calculations it modifies the DOM and displays it to the user.

Props

OK, let’s up the complexity of our component and add some props.

import React from 'react';

// We declare an interface so that TypeScript
// knows what kind of props we are expecting.
interface ListItems {
items: Array<number>
}

// Now we take in our list of items
const List = (listItems: ListItems) => {

// Here we map out each of our list items
// to JSX, which we can later render.
const listItemsLi = listItems.items.map(num => <li key={num}>{num}</li>)

return (
<>
<div>
<h1>Our list goes here!</h1>
{/* Here we render all of our list items */}
<ul>
{listItemsLi}
</ul>
</div>
</>
);
};

// Finally we export our component for use in
// our page.
export default List;

We render the results as below.

<main className={styles.main}>
<List items={[1, 2, 3, 4]}></List>
</main>

Which gives us the screen

Rendering a list.

Let’s summarise what happened. If we couldn’t pass in parameters to our components we would have to write new ones all the time, hence the adoption of props. We can use these to programatically write JSX, and render it in the browser.

One interesting question is, ‘in the trigger-render-commit cycle, does a change in props cause a re-render’? The answer is yes, but we can only change props if they are mutable (an object or array etc.).

Although we can mutate props, in reality, we never should. We think of props as parameters to a function. We can create a new component with different props, but it would cause unexpected side-effects if we mutated our parameters directly!

This emphasises how important pure functions are. We want our components to reliably render given the same inputs, and we don’t want making changes to randomly cause other bits of our page to display differently!

State

In order to alter our component we need to introduce another concept, state.

A lot of the time we would like our component to have some sort of memory. For example, we may want our to update the items in our list component, re-rendering based on its new contents.

We’ll need to introduce another concept, event handlers. These let us react to interactions with our page.

import React, { useState } from 'react';

// We declare an interface so that TypeScript
// knows what kind of props we are expecting.
interface ListItems {
items: Array<number>
}

// Now we take in our list of items
const List = (listItems: ListItems) => {

// Here we introduce our state, along with a
// method for updating it.
const [items, setItems] = useState(listItems.items)

// Here we introduce our event handler. Notice how
// we use the state mutation function and concat to
// change the array, we don't mutate the array directly.
// Instead we create a new array and pass it to the
// mutation function.
const handleClick = () => {
setItems(items.concat(Math.random()))
}

// Here we map out each of our list items
// to JSX, which we can later render.
const listItemsLi = items.map(num => <li key={num}>{num}</li>)

return (
<>
<div>
<h1>Our list goes here!</h1>
{/* Here we render all of our list items */}
<ul>
{listItemsLi}
</ul>
{/* Now we pass the handler, and have it triggered
when we click the button. Recognise we are passing
a reference to the method, not calling it! */}
<button onClick={handleClick}>
Add an item!
</button>
</div>
</>
);
};

// Finally we export our component for use in
// our page.
export default List;

Something important to note is that we don’t mutate the array directly, we instead make a copy, feeding it to the set function. By calling this we trigger the re-rendering.

Hopefully you appreciate the parent-child hierarchy React is based around. We have a top level component which creates components, which create components, which create components…

This helps us know where state should live. We demonstrate with the below.

Diagram representing where state should reside

If we have state that affects the components in green, we generally put it in the closest parent component of both (in purple) and feed it down as props. We have to put it at that level or higher in order to feed it down the hierarchy.

Event Handlers

Let’s circle back slightly, we introduced event handlers, but didn’t go into much detail.

At their core they are a way of handling user interactions, but beyond that there are a couple of extra noteworthy things.

The first is event propagation. Re-examine the hierarchy above. If we click one of the lower level components then the click event will propagate the whole way up to the top, calling all click handlers from the bottom up.

The second is passing properties into a component. We only have a few, basic built-in handlers (onClick etc.), we define our own by passing function props into a component, then using them alongside the built in ones.

import React from 'react';

// The interface we define for properties to
// be handed to our child element. The function
// signature is that for an onClick event handler
interface ChildProps {
handleChildClick: (event: React.MouseEvent<HTMLElement>) => void
}

// Here we define our parent component, which will
// contain our child component.
const ParentComponent = () => {

// This is the click handler in the parent, it will
// be triggered AFTER the child handler.
const handleParentClick = (event: React.MouseEvent<HTMLElement>) => {
// This is an important feature of event handling. We
// know that events will be passed up the component
// hierarchy. However, we can stop and change this using
// methods on the event which is emitted.
event.stopPropagation()
console.log("Parent component clicked")
}

// This is the child handler, it will be triggered
// BEFORE the parent handler.
const handleChildClick = () => {
console.log("Child component clicked")
}

// Now we defined the JSX which will be rendered, and
// pass an event handler to the child
return (
<div onClick={handleParentClick}>
<ChildComponent handleChildClick={handleChildClick}></ChildComponent>
</div>
);

};

// This is our child component! We pass in the event
// handler from the parent.
const ChildComponent = (childProps: ChildProps) => {

return (
<>
<button onClick={childProps.handleChildClick}>
Child button
</button>
</>
);
};

export default ParentComponent;

Refs

As long as you understand state, refs are fairly straightforward. The only difference is that state forces a component to re-render, while a ref does not. Most commonly they are used to manipulate the DOM.

import React, { useRef } from 'react';

const Refs = () => {

// Here we declare our ref. As we are using Typescript
// and initialising with null we need to tell it what
// type of ref we are expecting. The initialising with
// null is a common pattern and is updated below.
const myRef = useRef<HTMLInputElement>(null);

// An event handler used to focus on the input when
// we press the button
const handleClick = () => {
myRef.current?.focus()
}

return (
<>
<button onClick={handleClick}>
Press to focus!
</button>
{/* Here we pass in the reference and can
use it to focus */}
<input ref={myRef}></input>
</>
)

};

// Finally we export our component for use in
// our page.
export default Refs;

On rendering the page we are given an input field we can immediately focus on, as below.

Manipulating the DOM with refs

However, this goes against what React is designed to do. One of the brilliant things about the library is we don’t need to manually manipulate the DOM, we can let React do it for us.

Effects

Refs and Effects are referred to as ‘Escape Hatches’, they are ways of exiting the React paradigm. We have covered the former, now for the latter.

Occasionally we may want to synchronise with an external system once a component has rendered. The most common example of this I have seen is sending an analytics event to tell us an end-user has seen a certain part of the screen. This is the role of effects.

import React, { useEffect } from 'react';

const Effects = () => {

// This is how we declare a function that
// will be executed post-render.
useEffect(() => {
console.log("This will render second")
});

// This is executed before/ as part of the
// rendering process
console.log("This will render first")

// Here we arbitrarily render something
return (
<>
<div>
For rendering effects
</div>
</>
)

};

export default Effects;

Although we said we wouldn’t focus on Next.js, there is a point worth noting on server-side rendering. As useEffect is executed after the component is rendered, it will always execute on the client.

Context

Now we’re borderline React professionals, we’ve probably noticed an issue with the parent-child model and props. What if I have something at my top level component I would like to pass to a component 100 layers deep? Do I have to pass it as props through 99 components?

This is the role of context. With this we can declare a variable in a parent component that can be used in child components without being explicitly passed.

import React, { createContext, useContext } from 'react';

// Here we define our context. This is what we will pass
// around. It is implicitly a string with a default value.
const LevelContext = createContext("This should be overwritten");

// This is our top level component. Here we declare a provider,
// which is what exposes the value to the lower levels
const LevelOne = () => {

return (
<LevelContext.Provider value={"This is the level value"}>
<LevelTwo></LevelTwo>
</LevelContext.Provider>
)

};

// This is an empty component we use as a mid layer to demonstrate
// passing the context multiple layers without explicit props
const LevelTwo = () => {

return (
<LevelThree></LevelThree>
)

};

// Finally we use the context in our third layer down. Notice
// we need a function, and to tell it which context we are
// using
const LevelThree = () => {

const level = useContext(LevelContext);

return (
<>
<div>
The level is {level}
</div>
</>
)

};

export default LevelOne;

Conclusion

And that concludes our whistlestop tour! There is so much more surrounding React, including Redux, storybooks and testing, but they are different stories for a different time. Hope it was useful!

--

--

James Collerton

Senior Software Engineer at Spotify, Ex-Principal Engineer at the BBC