Building client-side apps with React
Learn how to create client-side apps using React and TypeScript
React makes dealing with the DOM in JavaScript more like writing HTML. It helps package up elements into "components" so you can divide your UI up into reusable pieces. You can try out the examples online by creating a new React playground.
Quick summary
A lot of React concepts are explained in detail below. If you just want to get started quickly here's a code sample with the most important features:
React elements
Interacting with the DOM can be awkward when you just want to render an element:
This is frustrating because there is a simpler, more declarative way to describe elements—HTML:
Unfortunately we can't use HTML inside JavaScript files. HTML is a static markup language—it can't create elements dynamically as a user interacts with our app. This is where React comes in:
This variable is a React element. It's created using a special syntax called JSX that lets us write HTML-like elements within our JavaScript.
The example above will be transformed into a JS function call that returns an object:
Templating dynamic values
JSX supports inserting dynamic values into your elements. It uses a similar syntax to JS template literals: anything inside curly brackets will be evaluated as a JS expression:
You can do all kinds of JS stuff inside the curly brackets, like referencing other variables, or conditional expressions.
Note on expressions
You can put any valid JS expression inside the curly brackets. An expression is code that resolves to a value. I.e. you can assign it to a variable. These are all valid expressions:
This is not a valid expression:
if
blocks are statements, not expressions. The main impact of this is that you have to use ternaries instead of if
statements inside JSX.
React components
React elements aren't very useful on their own, since they're just static objects. To build an interface we need something reusable and dynamic, like functions.
A React component is a function that returns a React element.
Valid elements
Your components don't have to return JSX. A React element can be JSX, or a string, number, boolean, or array of elements. Returning null
, undefined
, false
or ""
will cause your component to render nothing.
Returning an array is especially useful for rendering lists from data:
Array items in JSX must have a special unique key
prop so React can keep track of the order if the data changes.
Composing components
Components are useful because JSX allows us to compose them together just like HTML elements. We can use our Title
component as JSX within another component:
When we use a component in JSX (<Title />
) React will find the corresponding Title
function, call it, and use whatever element it returns.
Customising components
A component where everything is hard-coded isn't very useful. Functions are most useful when they take arguments. Passing different arguments lets us change what the function returns each time we call it.
JSX supports passing arguments to your components. It does this using the same syntax as HTML:
Most people name this object "props" in their component function:
We can use these props within your components to customise them. For example we can insert them into our JSX:
Now we can re-use our Title
component to render different DOM elements:
Typing our props
We need to define the type of the props object so TypeScript can check we're using the component correctly. We can do this in the same way we would type any object:
If you have several props it can be more readable to extract the type to an alias:
The React docs have a page on using TypeScript with React, which can be helpful.
Non-string props
Since JSX is JavaScript it supports passing any valid JS expression to your components, not just strings. To pass JS values as props you use curly brackets, just like interpolating expressions inside tags.
Children
It would be nice if we could nest our components just like HTML. Right now this won't work, since we hard-coded the text inside our <h1>
:
JSX supports a special prop to achieve this: children
. Whatever value you put between JSX tags will be passed to the component function as a prop named children
.
You can then access and use it exactly like any other prop.
Now this JSX will work as we expect:
This is quite powerful, as you can now nest your components to build up more complex DOM elements.
Typing the children
prop
children
propSince the children of an element can be almost anything React has built-in type for it: React.ReactNode
.
Rendering to the page
You may be wondering how we get these components to actually show up on the page. React manages the DOM for you, so you don't need to use document.createElement
/.appendChild
.
React consists of two libraries—the main React
library and a specific ReactDOM
library for rendering to the DOM. We can use ReactDOM
to render a component to the DOM.
It's common practice to have a single top-level App
component that contains all the rest of the UI.
Event listeners
JSX makes adding event listeners simple—you add them inline on the element you want to target. They are always formatted as "on" followed by the camelCased event name (onClick
, onKeyDown
etc):
React state
An app can't do much with static DOM elements—we need a way to create values that can change and trigger updates to the UI.
React provides a special "hook" function called useState
to create a stateful value. When you update the value React will automatically re-render the component to ensure the UI stays up-to-date.
When this button is clicked we want the count to go up one:
We need to use the useState
hook. It takes the initial state value as an argument, and returns an array. This array contains the state value itself, and a function that lets you update the state value.
It's common to use destructuring to shorten this:
If we call setCount(1)
React will re-run our Counter
component, but this time the count
variable will be 1
instead of 0
. This is how React keeps your UI in sync with the state.
Lifting state up
React components encapsulate their state—it lives inside that function and can't be accessed elsewhere. Sometimes however you need several components to read the same value. In these cases you should "lift the state up" to a shared parent component:
Here FancyButton
and FancyText
both need access to the state, so we move it up to Counter
and pass it down via props. That way both components can read/update the same state value.
Typing state
TS can mostly infer state types from the initial value you provide. In the Counter
example above it will infer count
to be of type number
, since the initial value is 0
. We can explicitly provide a type by passing a generic to useState
:
That's not useful here, but can be necessary if for example you wish to constrain the type:
We need to define a type for the state setter function when we pass it down to another component as a prop (like the FancyButton
example above). Since updating state does some React magic behind the scenes we can't just write a normal function type: we must use React's built-in types:
The React.Dispatch<React.SetStateAction<number>>
is a little wild because of the triple-nested generic, but the only part you ever need to change is the final generic (<number>
here). This should be the type of the actual state value.
Updates based on previous state
Sometimes your update depends on the previous state value. For example updating the count inside an interval. In these cases you can pass a function to the state updater. React will call this function with the previous state, and whatever you return will be set as the new state.
We cannot just reference count
, since this is 0
when the interval is created. It would just do 0 + 1
over and over (so the count would be stuck at 1
).
Form fields
React apps still use the DOM, so forms work the same way:
Typing DOM events
TS can infer the type of the event parameter in inline handlers. For example:
Here event
will be inferred as React.MouseEvent<HTMLButtonElement>
.
When you define event handlers as separate function (like the updateName
example above) you will need to manually provide this type. React defines types for most DOM events—you just need to pass in the type of DOM element it will be triggered for. For example:
Often the easiest way to find the type is to write the handler inline first, then copy the type that is inferred.
Controlled components
React tries to normalise the different form fields, so behaviour is consistent across e.g. <input>
and <select>
. If you need to keep track of values as they update you can add an onChange
listener and value
prop.
This pattern is often known as "controlled components".
Side effects
So far we've seen how React keeps your UI in sync with your data. Your components describe the UI using JSX and React updates the DOM as required. However apps sometimes need to sync with something else, like fetching from an API or setting up a timer.
These are known as "side effects", and they can't be represented with JSX. This means we need a different way to ensure our side effects stay in sync just like our UI.
Using effects
React provides another "hook" like useState()
for running side-effects after your component renders. It's called useEffect()
. It takes a function as an argument, which will be run after every render by default.
Here's our counter, with an effect to sync the document title with the count:
Calling setCount
will trigger a re-render, which will cause the Effect to re-run, so the title will stay in sync with our state.
Skipping effects
By default all the Effects in a component will re-run after every render of that component. This ensures the Effect always has the correct state values. However what if we had multiple state values? Updating unrelated state would re-run the Effect even if count
hadn't changed.
useEffect()
takes a second argument: an array of dependencies for the Effect. Any variable used inside your Effect function should go into this array:
Now the Effect will only re-run if the value of count
has changed.
Effects with no dependencies
Sometimes your Effect will not be dependent on any props or state. In this case you can pass an empty array, to signify that the Effect has no dependencies and shouldn't need to be re-run.
Here we want to show what key the user pressed, so we need an event listener on the window. This listener only needs to be added once:
Without the empty dependency array we would end up adding a new event listener every time the Effect re-ran. This could cause performance problems.
Cleaning up effects
It's important your Effects can clean up after themselves. Otherwise they might leave their side-effects around when the component is "unmounted" (e.g. if the user navigates to another page).
Our previous example needs to make sure the event listener is removed from the window. We can tell React to do this by returning a function from the Effect. React will call this function whenever it needs to clean up: both when the component is unmounted and before re-running the Effect.
React helps you remember to do this by running Effects twice during development (even if you pass an empty dependency array). This is designed to help you catch places where you forgot to clean up your Effect.
Last updated