React Fac
challenge: https://github.com/foundersandcoders/react-challenge
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:
ES Modules
ES Modules are a way to isolate your JS files. This is similar to Node’s require
syntax, but built-in to the JS language (rather than a proprietary Node feature). React apps usually use ES Modules because they’re built for the browser rather than the server.
Modern browsers support modules loaded from a script tag with type="module"
. This tells the browser that this script may load more code from other files.
Generally (for wider browser support) apps use a tool called a “bundler” to parse all the imports and bundle them into a single file that older browsers will understand.
Exports
Files can have two types of exports: “default” and “named”. A file can only have one default export, but can have many named exports.
This is how you create a default export:
This is how you create a named export:
You can only default export a single thing, but you can have as many named exports as you like:
You don’t have to export things at the end of the file. You can do it inline:
Imports
There are also two kinds of imports: default and named. The way you import a value must match the way you exported it.
This is how you import something that was default-exported:
{% box %}
When you import a default-export you can call it whatever you want.
{% endbox %}
Named-exports must be imported with curly brackets:
You can import as many named-exports as you like on the same line:
{% box %}
Named-exports must be imported with the correct name.
{% endbox %}
In the browser import paths must be valid URIs (e.g. "./maths.js"
or "https://cdn.pika.dev/lower-case@^2.0.1"
). This means you must include the file extension for local files. Node and bundlers often support leaving this off for convenience however.
{% box %}
Unlike require
, import
is not dynamic—you cannot use a variable in an import path. Imports are static and must live at the top of the file.
{% endbox %}
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.
{% box %}
JSX is not valid JavaScript. It’s a special syntax to make creating DOM elements more like writing HTML. Browsers don’t understand it, so React code has to be processed using a tool like Vite before it’s run.
Some tools require the .jsx
file extension to indicate non-standard syntax.
{% endbox %}
The example above will be transformed into a JS function call that returns an object:
{% box “error” %}
Important warning
Since JSX is closer to JS than HTML we have to use the camelCase versions of HTML attributes: class
becomes className
, for
becomes htmlFor
and tabindex
becomes tabIndex
etc.
Also self-closing tags (like <img>
) must have a closing slash: <img />
. This is optional in HTML but required in JSX.
{% endbox %}
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.
{% box %}
Components are normal JS functions, which means they can only return one thing. The following JSX is invalid:
since the Thing
function is trying to return two objects. The solution to this is to wrap sibling elements in a parent <div>
(or use a Fragment).
{% endbox %}
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.
{% box %}
You have to capitalise your component names. This is how JSX distinguishes between HTML and React components. E.g. <img />
will create an HTML image tag, but <Img />
will look for a component function named Img
.
{% endbox %}
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:
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.
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 use the ReactDOM.render()
function 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.
{% box %}
You only call ReactDOM.render()
once per app. You give it the very top-level component of your app and it will move down the component tree rendering all the children inside of it.
{% endbox %}
{% disclosure “A bit more detail (if you’re interested)” %}
The component functions return React elements, which are objects describing an element, its properties, and its children. These objects form a tree, with a top-level element that renders child elements, that in turn have their own children. Here is a small React component that renders a couple more:
<App />
tells React to call the App
function and pass in any child elements as props.children
. This returns an object roughly like this:
This object is passed to ReactDOM.render
, which will loop through every property. If it finds a string type (e.g. “p”) it’ll create a DOM node. If it finds a function type it’ll call the function with the right props to get the elements that component returns. It keeps doing this until it runs out of elements to render.
This is the final DOM created for this app:
{% enddisclosure %}
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.
{% box “error” %}
Never change a state variable directly. React needs to know about changes, otherwise it won’t re-render the component. For example count++
will change the old copy of the state value but won’t re-run the component.
{% endbox %}
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.
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:
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.
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.
{% box %}
Effects can be a little complicated, and devs often use them unnecessarily. Try to prefer solving problems with state and event listeners, and save the Effects for things that cannot be done with JSX.
{% endbox %}
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.
{% box %}
We can’t just put the document.title
line directly in our component body, because React components should be “pure”. This means they should just return JSX and have no side-effects as a result of rendering.
{% endbox %}
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.