diff --git a/src/learn/react/index.md b/src/learn/react/index.md index 0d8d6db6..fb8297c6 100644 --- a/src/learn/react/index.md +++ b/src/learn/react/index.md @@ -1,24 +1,24 @@ --- title: Building client-side apps with React -description: Learn how to create client-side apps using React +description: Learn how to create client-side apps using React and TypeScript tags: - workshop keywords: - - js + - ts - react - jsx 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](https://vite.new/react). +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](https://vite.new/react-ts). ## 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: -```jsx -import { useState } from "react"; -import ReactDOM from "react-dom"; +```tsx +import React, { useState } from "react"; +import ReactDOM from "react-dom/client"; function Counter() { // Calling the `setCount` with a new value re-runs your component @@ -27,7 +27,7 @@ function Counter() { } // Any properties passed to the component are available on the `props` object -function Title(props) { +function Title(props: { id: string; children: React.ReactNode }) { return

{props.children}

; } @@ -41,117 +41,19 @@ function App() { } // React handles all DOM element creation/updates—you just call `render` once -const root = ReactDOM.createRoot(document.querySelector("#root")); +const root = ReactDOM.createRoot(document.querySelector("#root")!); root.render( - - + + ); ``` -## 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](https://caniuse.com/#search=modules) support modules loaded from a script tag with `type="module"`. This tells the browser that this script may load more code from other files. - -```html - -``` - -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: - -```js -const a = 1; -export default a; -``` - -This is how you create a named export: - -```js -const a = 1; -export { a }; -``` - -You can only default export a single thing, but you can have as many named exports as you like: - -```js -const a = 1; -const b = 2; -export { a, b }; -``` - -You don't have to export things at the end of the file. You can do it inline: - -```js -export const a = 1; -export const b = 2; -``` - -### 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: - -```js -// index.js -import a from "./maths.js"; -console.log(a); // 1; -``` - -{% box %} - -When you import a default-export you can call it whatever you want. - -{% endbox %} - -Named-exports must be imported with curly brackets: - -```js -// index.js -import { a } from "./maths.js"; -console.log(a); // 1; -``` - -You can import as many named-exports as you like on the same line: - -```js -import { a, b } from "./maths.js"; -console.log(a, b); // 1 2 -``` - -{% 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: -```js +```ts const title = document.createElement("h1"); title.className = "title"; title.textContent = "Hello world!"; @@ -165,7 +67,7 @@ This is frustrating because there is a simpler, more declarative way to describe 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: -```jsx +```tsx const title =

Hello world!

; ``` @@ -181,7 +83,7 @@ Some tools require the `.jsx` file extension to indicate non-standard syntax. The example above will be transformed into a JS function call that returns an object: -```js +```ts const title = _jsx("h1", { className: "title", children: "Hello world!" }); /* * Over-simplified for examples sake: @@ -209,20 +111,20 @@ Also self-closing tags (like ``) **must** have a closing slash: ``. 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: -```jsx +```tsx const title =

Hello {5 * 5}

; //

Hello 25

``` You can do all kinds of JS stuff inside the curly brackets, like referencing other variables, or conditional expressions. -```jsx +```tsx const name = "oli"; const title =

Hello {name}

; //

Hello oli

``` -```jsx +```tsx const number = Math.random(); const result =
{number > 0.5 ? "You won!" : "You lost"}
; // 50% of the time:
You won!
@@ -233,7 +135,7 @@ const result =
{number > 0.5 ? "You won!" : "You lost"}
; 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: -```js +```ts const number = 5 + 4 * 9; const isEven = number % 2 === 0; const message = isEven ? "It is even" : "It is odd"; @@ -241,7 +143,7 @@ const message = isEven ? "It is even" : "It is odd"; This is _not_ a valid expression: -```js +```ts const message = if (isEven) { "It is even" } else { "It is odd" }; // this is not valid JS and will cause an error ``` @@ -256,7 +158,7 @@ React elements aren't very useful on their own, since they're just static object A _React component_ is a function that returns a React element. -```jsx +```tsx function Title() { return

Hello world!

; } @@ -268,7 +170,7 @@ Your components don't _have_ to return JSX. A React element can be JSX, or a str Returning an array is especially useful for [rendering lists](https://react.dev/learn/rendering-lists) from data: -```jsx +```tsx const fruits = ["apple", "orange", "banana"]; function FruitList() { @@ -283,7 +185,7 @@ Array items in JSX must have a special unique [`key` prop](https://react.dev/lea Components are normal JS functions, which means they can **only return one thing**. The following JSX is invalid: -```jsx +```tsx // This won't work! function Thing() { return ( @@ -293,7 +195,18 @@ function Thing() { } ``` -since the `Thing` function is trying to return _two_ objects. The solution to this is to wrap sibling elements in a parent `
` (or use a [Fragment](https://react.dev/reference/react/Fragment#returning-multiple-elements)). +since the `Thing` function is trying to return _two_ objects. The solution to this is to wrap sibling elements in a [Fragment](https://react.dev/reference/react/Fragment#returning-multiple-elements). + +```tsx +function Thing() { + return ( + <> + Hello + Goodbye + + ); +} +``` {% endbox %} @@ -301,7 +214,7 @@ since the `Thing` function is trying to return _two_ objects. The solution to th 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: -```jsx +```tsx function Title() { return

Hello world!

; } @@ -329,7 +242,7 @@ A component where everything is hard-coded isn't very useful. Functions are most JSX supports passing arguments to your components. It does this using the same syntax as HTML: -```jsx +```tsx /** * The above JSX is transformed into this: @@ -339,7 +252,7 @@ JSX supports passing arguments to your components. It does this using the same s Most people name this object "props" in their component function: -```jsx +```tsx function Title(props) { console.log(props); // { name: "oli" } return <h1 className="title">Hello world</h1>; @@ -348,7 +261,7 @@ function Title(props) { We can use these props within your components to customise them. For example we can insert them into our JSX: -```jsx +```tsx function Title(props) { return <h1 className="title">Hello {props.name}</h1>; } @@ -356,7 +269,7 @@ function Title(props) { Now we can re-use our `Title` component to render different DOM elements: -```jsx +```tsx function Page() { return ( <div className="page"> @@ -373,17 +286,39 @@ function Page() { */ ``` +### 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: + +```tsx +function Title(props: { name: string }) { + return <h1 className="title">Hello {props.name}</h1>; +} +``` + +If you have several props it can be more readable to extract the type to an alias: + +```tsx +type TitleProps = { name: string }; + +function Title(props: TitleProps) { + return <h1 className="title">Hello {props.name}</h1>; +} +``` + +The React docs have a page on [using TypeScript with React](https://react.dev/learn/typescript), 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. -```jsx +```tsx function Page() { const fullname = "oliver" + " phillips"; return ( <div className="page"> <Title name={fullname} /> - <Title name={5 * 5} /> + <Title name={String(5 * 5)} /> </div> ); } @@ -399,7 +334,7 @@ function Page() { 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 +```tsx <Title>Hello oli /** * The above JSX is transformed into this: @@ -411,7 +346,7 @@ JSX supports a special prop to achieve this: `children`. Whatever value you put You can then access and use it exactly like any other prop. -```jsx +```tsx function Title(props) { return

{props.children}

; } @@ -419,14 +354,14 @@ function Title(props) { Now this JSX will work as we expect: -```jsx +```tsx Hello oli //

Hello oli

``` This is quite powerful, as you can now nest your components to build up more complex DOM elements. -```jsx +```tsx // pretend we have defined Image and BigText components above <Image src="hand-wave.svg" /> @@ -434,16 +369,27 @@ This is quite powerful, as you can now nest your components to build up more com ``` +### Typing the `children` prop + +Since the children of an element can be almost anything React has built-in type for it: `React.ReactNode`. + +```tsx +type TitleProps = { children: React.ReactNode }; +function Title(props) { + return

{props.children}

; +} +``` + ## 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. +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. -```jsx -import ReactDOM from "react-dom"; +```tsx +import ReactDOM from "react-dom/client"; function App() { return ( @@ -454,12 +400,14 @@ function App() { ); } -ReactDOM.render(, document.querySelector("#root")); +const div = document.querySelector("#root")!; // The `!` tells TS it's definitely not null +const root = ReactDOM.createRoot(div); +root.render(); ``` {% 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. +You only call `render()` **once per app**. You pass 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 %} @@ -467,8 +415,8 @@ You only call `ReactDOM.render()` **once per app**. You give it the very top-lev 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: -```jsx -import ReactDOM from "react-dom"; +```tsx +import ReactDOM from "react-dom/client"; function App() { return ( @@ -479,7 +427,7 @@ function App() { ); } -ReactDOM.render(, document.querySelector("#root")); +ReactDOM.createRoot(document.querySelector("#root")!).render(); ``` `` tells React to call the `App` function and pass in any child elements as `props.children`. This returns an object roughly like this: @@ -507,7 +455,7 @@ ReactDOM.render(, document.querySelector("#root")); } ``` -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 object is passed to `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: @@ -524,9 +472,9 @@ This is the final DOM created for this app: ## 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): +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): -```jsx +```tsx function Alerter() { return ; } @@ -540,8 +488,8 @@ React provides a special "hook" function called `useState` to create a stateful When this button is clicked we want the count to go up one: -```jsx -function Counter(props) { +```tsx +function Counter() { const count = 0; return ; } @@ -549,10 +497,10 @@ function Counter(props) { 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. -```jsx +```tsx import { useState } from "react"; -function Counter(props) { +function Counter() { const stateArray = useState(0); const count = stateArray[0]; const setCount = stateArray[1]; @@ -562,7 +510,7 @@ function Counter(props) { It's common to use destructuring to shorten this: -```jsx +```tsx import { useState } from "react"; function Counter(props) { @@ -583,7 +531,7 @@ If we call `setCount(1)` React will re-run our `Counter` component, but this tim 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"](https://react.dev/learn/sharing-state-between-components) to a shared parent component: -```jsx +```tsx function Counter() { const [count, setCount] = useState(0); return ( @@ -599,7 +547,7 @@ function FancyButton(props) { props.setCount(props.count + 1); } return ( - ); @@ -612,11 +560,47 @@ function FancyText(props) { 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`: + +```ts +const [count, setCount] = useState(0); +``` + +That's not useful here, but can be necessary if for example you wish to constrain the type: + +```ts +type Status = "loading" | "complete" | "error"; +const [status, setStatus] = useState("loading"); +``` + +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: + +```tsx +type FancyButtonProps = { + count: number; + setCount: React.Dispatch>; +}; +function FancyButton(props: FancyButtonProps) { + function increment() { + props.setCount(props.count + 1); + } + return ( + + ); +} +``` + +The `React.Dispatch>` is a little wild because of the triple-nested generic, but the only part you ever need to change is the final generic (`` 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_](https://react.dev/apis/react/useState#updating-state-based-on-the-previous-state) to the state updater. React will call this function with the previous state, and whatever you return will be set as the new state. -```jsx +```tsx // ... const [count, setCount] = useState(0); // ... @@ -638,7 +622,7 @@ We cannot just reference `count`, since this is `0` when the interval is created React apps still use the DOM, so forms work the same way: -```jsx +```tsx function ChooseName() { const [name, setName] = useState(""); @@ -657,13 +641,36 @@ function ChooseName() { } ``` +### Typing DOM events + +TS can infer the type of the event parameter in inline handlers. For example: + +```tsx + +``` + +Here `event` will be inferred as `React.MouseEvent`. + +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](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b580df54c0819ec9df62b0835a315dd48b8594a9/types/react/index.d.ts#L1247C1-L1373)—you just need to pass in the type of DOM element it will be triggered for. For example: + +```ts +function updateName(event: React.FormEvent) { + event.preventDefault(); + setName(event.target.username.value); +} +``` + +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. `` and `