From 5082dcb52d611e310881ddd249dbdce78ffadb14 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Sat, 9 Jan 2021 12:32:06 -0700 Subject: [PATCH 1/3] migrate to TypeScript --- .github/workflows/validate.yml | 2 + package-lock.json | 3 +- package.json | 10 +- scripts/remove-ts | 2 + src/__tests__/{05.js => 06.tsx} | 4 +- .../{06.extra-2.js => 07.extra-2.tsx} | 11 +- .../{06.extra-3.js => 07.extra-3.tsx} | 11 +- src/__tests__/{06.js => 07.tsx} | 11 +- src/__tests__/{07.js => 08.tsx} | 12 +- src/exercise/01.html | 15 +- src/exercise/01.md | 8 +- src/exercise/02.html | 7 +- src/exercise/02.md | 71 +++- src/exercise/03.md | 68 +++- src/exercise/04.extra-4.html | 41 ++ src/exercise/04.md | 191 +++++----- src/exercise/05.md | 351 ++++++++++++++---- src/exercise/05.tsx | 40 ++ src/exercise/06.md | 191 +++++----- src/exercise/{05.js => 06.tsx} | 2 +- src/exercise/07.md | 188 ++++++---- src/exercise/{06.js => 07.tsx} | 11 +- src/exercise/08.md | 97 +++++ src/exercise/{07.js => 08.tsx} | 24 +- src/final/01.extra-1.html | 26 +- src/final/01.html | 24 +- src/final/02.extra-1.html | 14 +- src/final/02.extra-2.html | 24 ++ src/final/03.extra-3.html | 22 ++ src/final/03.extra-4.html | 22 ++ src/final/04.extra-3.html | 36 +- src/final/04.extra-4.html | 32 +- src/final/04.extra-5.html | 33 -- src/final/05.extra-1.tsx | 41 ++ src/final/05.extra-2.tsx | 41 ++ src/final/05.extra-3.tsx | 41 ++ src/final/05.extra-4.tsx | 43 +++ src/final/05.extra-5.tsx | 47 +++ src/final/05.tsx | 40 ++ src/final/{05.extra-1.js => 06.extra-1.tsx} | 8 +- src/final/{05.extra-2.js => 06.extra-2.tsx} | 11 +- src/final/06.js | 28 -- src/final/{05.js => 06.tsx} | 3 +- src/final/{06.extra-1.js => 07.extra-1.tsx} | 19 +- src/final/{06.extra-2.js => 07.extra-2.tsx} | 28 +- src/final/{06.extra-3.js => 07.extra-3.tsx} | 30 +- src/final/07.tsx | 40 ++ src/final/{07.extra-1.js => 08.extra-1.tsx} | 14 +- src/final/{07.js => 08.tsx} | 24 +- src/{index.js => index.tsx} | 0 src/react-app-env.d.ts | 1 + src/{setupTests.js => setupTests.ts} | 0 tsconfig.json | 20 + 53 files changed, 1496 insertions(+), 587 deletions(-) create mode 100755 scripts/remove-ts rename src/__tests__/{05.js => 06.tsx} (95%) rename src/__tests__/{06.extra-2.js => 07.extra-2.tsx} (79%) rename src/__tests__/{06.extra-3.js => 07.extra-3.tsx} (69%) rename src/__tests__/{06.js => 07.tsx} (69%) rename src/__tests__/{07.js => 08.tsx} (72%) create mode 100644 src/exercise/04.extra-4.html create mode 100644 src/exercise/05.tsx rename src/exercise/{05.js => 06.tsx} (97%) rename src/exercise/{06.js => 07.tsx} (83%) create mode 100644 src/exercise/08.md rename src/exercise/{07.js => 08.tsx} (71%) create mode 100644 src/final/02.extra-2.html create mode 100644 src/final/03.extra-3.html create mode 100644 src/final/03.extra-4.html delete mode 100644 src/final/04.extra-5.html create mode 100644 src/final/05.extra-1.tsx create mode 100644 src/final/05.extra-2.tsx create mode 100644 src/final/05.extra-3.tsx create mode 100644 src/final/05.extra-4.tsx create mode 100644 src/final/05.extra-5.tsx create mode 100644 src/final/05.tsx rename src/final/{05.extra-1.js => 06.extra-1.tsx} (85%) rename src/final/{05.extra-2.js => 06.extra-2.tsx} (81%) delete mode 100644 src/final/06.js rename src/final/{05.js => 06.tsx} (92%) rename src/final/{06.extra-1.js => 07.extra-1.tsx} (53%) rename src/final/{06.extra-2.js => 07.extra-2.tsx} (53%) rename src/final/{06.extra-3.js => 07.extra-3.tsx} (50%) create mode 100644 src/final/07.tsx rename src/final/{07.extra-1.js => 08.extra-1.tsx} (85%) rename src/final/{07.js => 08.tsx} (70%) rename src/{index.js => index.tsx} (100%) create mode 100644 src/react-app-env.d.ts rename src/{setupTests.js => setupTests.ts} (100%) create mode 100644 tsconfig.json diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d89a0ccd2..d169dac6b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,9 +3,11 @@ on: push: branches: - 'main' + - 'next' pull_request: branches: - 'main' + - 'next' jobs: setup: # ignore all-contributors PRs diff --git a/package-lock.json b/package-lock.json index bc8c98bc8..9facb4c10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18196,8 +18196,7 @@ "typescript": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", - "dev": true + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==" }, "typographic-apostrophes": { "version": "1.1.1", diff --git a/package.json b/package.json index b603206c5..bc1136b9b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "version": "1.0.0", "private": true, "keywords": [], - "homepage": "http://react-fundamentals.netlify.app/", + "homepage": "http://react-fundamentals-next.netlify.app/", "license": "GPL-3.0-only", "main": "src/index.js", "engines": { @@ -17,12 +17,15 @@ "@kentcdodds/react-workshop-app": "^4.0.0", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.1", + "@types/jest": "^26.0.20", + "@types/node": "^14.14.31", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", "chalk": "^4.1.0", "codegen.macro": "^4.1.0", "react": "^17.0.1", - "react-dom": "^17.0.1" + "react-dom": "^17.0.1", + "typescript": "^4.1.3" }, "devDependencies": { "husky": "^4.3.8", @@ -40,7 +43,8 @@ "setup": "node setup", "lint": "eslint .", "format": "prettier --write \"./src\"", - "validate": "npm-run-all --parallel build test:coverage lint" + "typecheck": "tsc", + "validate": "npm-run-all --parallel build test:coverage lint typecheck" }, "husky": { "hooks": { diff --git a/scripts/remove-ts b/scripts/remove-ts new file mode 100755 index 000000000..ebfeedaf8 --- /dev/null +++ b/scripts/remove-ts @@ -0,0 +1,2 @@ +npx https://gist.github.com/kentcdodds/e49b9b8da0dd467149d8d67258251585 +./scripts/fix-links diff --git a/src/__tests__/05.js b/src/__tests__/06.tsx similarity index 95% rename from src/__tests__/05.js rename to src/__tests__/06.tsx index dde4875d8..14d116575 100644 --- a/src/__tests__/05.js +++ b/src/__tests__/06.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import chalk from 'chalk' import {render, screen, prettyDOM} from '@testing-library/react' -import App from '../final/05' -// import App from '../exercise/05' +import {App} from '../final/06' +// import {App} from '../exercise/06' test('renders the correct styles new', () => { const {container} = render() diff --git a/src/__tests__/06.extra-2.js b/src/__tests__/07.extra-2.tsx similarity index 79% rename from src/__tests__/06.extra-2.js rename to src/__tests__/07.extra-2.tsx index 4bc2c8c48..77edd01c9 100644 --- a/src/__tests__/06.extra-2.js +++ b/src/__tests__/07.extra-2.tsx @@ -2,20 +2,21 @@ import * as React from 'react' import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/06.extra-2' -// import App from '../exercise/06' +import {App} from '../final/07.extra-2' +// import {App} from '../exercise/07' +let alert = jest.spyOn(global, 'alert') beforeAll(() => { - jest.spyOn(global, 'alert').mockImplementation(() => {}) + alert.mockImplementation(() => {}) }) beforeEach(() => { - global.alert.mockClear() + alert.mockClear() }) test('calls the onSubmitUsername handler when the submit is fired', () => { render() - const input = screen.getByLabelText(/username/i) + const input = screen.getByLabelText(/username/i) as HTMLInputElement const submit = screen.getByText(/submit/i) let value = 'A' diff --git a/src/__tests__/06.extra-3.js b/src/__tests__/07.extra-3.tsx similarity index 69% rename from src/__tests__/06.extra-3.js rename to src/__tests__/07.extra-3.tsx index e09413535..f33709ca8 100644 --- a/src/__tests__/06.extra-3.js +++ b/src/__tests__/07.extra-3.tsx @@ -1,20 +1,21 @@ import * as React from 'react' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/06.extra-3' -// import App from '../exercise/06' +import {App} from '../final/07.extra-3' +// import {App} from '../exercise/07' +let alert = jest.spyOn(global, 'alert') beforeAll(() => { - jest.spyOn(global, 'alert').mockImplementation(() => {}) + alert.mockImplementation(() => {}) }) beforeEach(() => { - global.alert.mockClear() + alert.mockClear() }) test('calls the onSubmitUsername handler when the submit is fired', () => { render() - const input = screen.getByLabelText(/username/i) + const input = screen.getByLabelText(/username/i) as HTMLInputElement const submit = screen.getByText(/submit/i) const value = 'A' diff --git a/src/__tests__/06.js b/src/__tests__/07.tsx similarity index 69% rename from src/__tests__/06.js rename to src/__tests__/07.tsx index 74f66515e..dbd271e8a 100644 --- a/src/__tests__/06.js +++ b/src/__tests__/07.tsx @@ -1,20 +1,21 @@ import * as React from 'react' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/06' -// import App from '../exercise/06' +import {App} from '../final/07' +// import {App} from '../exercise/07' +let alert = jest.spyOn(global, 'alert') beforeAll(() => { - jest.spyOn(global, 'alert').mockImplementation(() => {}) + alert.mockImplementation(() => {}) }) beforeEach(() => { - global.alert.mockClear() + alert.mockClear() }) test('calls the onSubmitUsername handler when the submit is fired', () => { render() - const input = screen.getByLabelText(/username/i) + const input = screen.getByLabelText(/username/i) as HTMLInputElement const submit = screen.getByText(/submit/i) const username = 'jenny' diff --git a/src/__tests__/07.js b/src/__tests__/08.tsx similarity index 72% rename from src/__tests__/07.js rename to src/__tests__/08.tsx index 7bc83340a..9040b2159 100644 --- a/src/__tests__/07.js +++ b/src/__tests__/08.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import {render, screen, within} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import App from '../final/07' -// import App from '../exercise/07' +import {App} from '../final/08' +// import {App} from '../exercise/08' test('renders', () => { const {container} = render() @@ -14,6 +14,9 @@ test('renders', () => { const orangeInput = screen.getByLabelText(/orange/i) const orangeContainer = screen.getByText(/orange/i).closest('li') + if (!orangeContainer) { + throw new Error(`🚨 Can't find the li for "orange"`) + } const inOrange = within(orangeContainer) userEvent.type(orangeInput, 'sup dawg') userEvent.click(inOrange.getByText('remove')) @@ -22,6 +25,11 @@ test('renders', () => { Array.from(allLis).forEach(li => { const label = li.querySelector('label') const input = li.querySelector('input') + if (!label || !input) { + throw new Error( + '🚨 Please make sure to not remove the label and input in the li elements', + ) + } expect(label.textContent).toBe(input.value) }) }) diff --git a/src/exercise/01.html b/src/exercise/01.html index 834757f37..31aca293e 100644 --- a/src/exercise/01.html +++ b/src/exercise/01.html @@ -1,16 +1,19 @@ - + - - + + + - - + + + + + - diff --git a/src/exercise/01.md b/src/exercise/01.md index f9f4ab1da..d3528bc3a 100644 --- a/src/exercise/01.md +++ b/src/exercise/01.md @@ -56,6 +56,8 @@ going to use React at all. Instead we're going to use JavaScript to create a `div` DOM node with the text "Hello World" and insert that DOM node into the document. +Now, open `exercise/01.html` and follow the instructions in there. + ## Extra Credit ### 1. 💯 generate the root node @@ -63,7 +65,11 @@ document. [Production deploy](http://react-fundamentals.netlify.app/isolated/final/01.extra-1.html) Rather than having the `root` node in the HTML, see if you can create that one -using JavaScript as well. +using JavaScript as well. Remove the `
` from the HTML and +instead of trying to find it with `document.getElementById('root')`, create it +and append it to the `document.body`. + +Do this in `exercise/01.html`, building on top of the work you've already done. ## 🦉 Feedback diff --git a/src/exercise/02.html b/src/exercise/02.html index 31ff2bea3..4ac8a3c31 100644 --- a/src/exercise/02.html +++ b/src/exercise/02.html @@ -15,14 +15,15 @@ // You're going to re-implement this code using React! // 💣 So go ahead and delete this implementation (or comment it out for now) // These three lines are similar to React.createElement + // 📜 https://reactjs.org/docs/react-api.html#createelement const element = document.createElement('div') - element.textContent = 'Hello World' // 💰 in React, you set this with the "children" prop element.className = 'container' // 💰 in React, this is also called the "className" prop + element.append('Hello World') // 💰 in React, you set this with the "children" prop // This is similar to ReactDOM.render + // 📜 https://reactjs.org/docs/react-dom.html#render rootElement.append(element) - // 🐨 Please re-implement the regular document.createElement code above - // with these React API calls + // 🐨 Please replace all the DOM-related code above with React API calls // 💰 The example in the markdown file should be a good hint for you. diff --git a/src/exercise/02.md b/src/exercise/02.md index cfcfc71c5..ade9d067b 100644 --- a/src/exercise/02.md +++ b/src/exercise/02.md @@ -10,19 +10,19 @@ React is the most widely used frontend framework in the world and it's using the same APIs that you're using when it creates DOM nodes. > In fact, -> [here's where that happens in the React source code](https://github.com/facebook/react/blob/48907797294340b6d5d8fecfbcf97edf0691888d/packages/react-dom/src/client/ReactDOMComponent.js#L416) +> [here's where that happens in the React source code](https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-dom/src/client/ReactDOMComponent.js#L416) > at the time of this writing. React abstracts away the imperative browser API from you to give you a much more declarative API to work with. > Learn more about the difference between those two concepts here: -> [Imperative vs Declarative Programming](https://tylermcginnis.com/imperative-vs-declarative-programming/) +> [Imperative vs Declarative Programming](https://ui.dev/imperative-vs-declarative-programming/) One important thing to know about React is that it supports multiple platforms -(for example, native and web). Each of these platforms has its own code -necessary for interacting with that platform, and then there's shared code -between the platforms. +(for example, native mobile, native desktop, web, and even terminal and VR). +Each of these platforms has its own code necessary for interacting with that +platform, and then there's shared code between the platforms. With that in mind, you need two JavaScript files to write React applications for the web: @@ -54,15 +54,43 @@ Once you include the script tags, you'll have two new global variables to use: Here's a simple example of the API: -```javascript +```typescript const elementProps = {id: 'element-id', children: 'Hello world!'} const elementType = 'h1' const reactElement = React.createElement(elementType, elementProps) ReactDOM.render(reactElement, rootElement) ``` +"Props" is short for "properties" and they're a foundational part of React +elements. You can think of the element type as a blueprint for the kind of React +component to create and the props are the inputs for when React actually creates +that element. + +`children` is a special prop. You can pass a single element like above, or an +array of children (if there's more than one). You can also pass the children as +any number of additional arguments: + +```typescript +const elementProps = {id: 'element-id'} +const child1 = 'Hello' +const child2 = ' ' +const child3 = 'world!' +const elementType = 'h1' +const reactElement = React.createElement( + elementType, + elementProps, + child1, + child2, + child3, +) +ReactDOM.render(reactElement, rootElement) +``` + Alright! Let's do this! +💰 Tip: `console.log` the `reactElement` to see what it looks like. You might +find it kinda interesting! + ## Extra Credit ### 1. 💯 nesting elements @@ -73,14 +101,29 @@ See if you can figure out how to write the JavaScript + React code to generate this DOM output: ```html - -
-
- Hello - World -
-
- +
    +
  • Green eggs
  • +
  • Ham
  • +
+``` + +Note also, depending on how you implement this, you may get a warning in the +developer console about needing a "key" prop. We'll get to that later. + +### 2. 💯 deep nesting elements + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/02.extra-2.html) + +Let's go a little deeper. Try to create this: + +```html +
+

Here's Sam's favorite food:

+
    +
  • Green eggs
  • +
  • Ham
  • +
+
``` ## 🦉 Feedback diff --git a/src/exercise/03.md b/src/exercise/03.md index e7d4c76b5..7b7e4920b 100644 --- a/src/exercise/03.md +++ b/src/exercise/03.md @@ -11,11 +11,14 @@ reading the code. It's fairly simple HTML-like syntactic sugar on top of the raw React APIs: ```jsx -const ui =

Hey there

+const element =

Hey there

// ↓ ↓ ↓ ↓ compiles to ↓ ↓ ↓ ↓ -const ui = React.createElement('h1', {id: 'greeting', children: 'Hey there'}) +const element = React.createElement('h1', { + id: 'greeting', + children: 'Hey there', +}) ``` Because JSX is not actually JavaScript, you have to convert it using something @@ -56,7 +59,7 @@ into something else." Let's take template literals for example: -```javascript +```typescript const greeting = 'Sup' const subject = 'World' const message = `${greeting} ${subject}` @@ -108,6 +111,65 @@ See if you can figure out how to make that work with JSX. 📜 https://reactjs.org/docs/jsx-in-depth.html#spread-attributes +### 3. 💯 deep nesting elements with JSX + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/03.extra-3.html) + +Remember when we created this with `React.createElement`? + +```html +
+

Here's Sam's favorite food:

+
    +
  • Green eggs
  • +
  • Ham
  • +
+
+``` + +That was... intellectually stimulating 😅. Well, now try doing the same thing +using JSX. I promise it'll be more fun this time. + +💰 Tip: remember `class` in JSX is `className`. + +### 4. 💯 using React Fragments + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/03.extra-4.html) + +One feature of JSX that you'll find useful is called +["React Fragments"](https://reactjs.org/docs/fragments.html). It's a special +kind of component from React which allows you to position two elements +side-by-side rather than just nested. + +Let's say we wanted to do the same thing as above, except we didn't want the +`container` `div`. So, we wanted to just create: + +```html +

Here's Sam's favorite food:

+
    +
  • Green eggs
  • +
  • Ham
  • +
+``` + +In React, we do this with ``. Replace the +`
` with a fragment and inspect the DOM to notice that +the elements are both rendered as direct children of `root`. + +💰 TIP: Fragments are common enough that there's a syntax shortcut for them. You +can open with `<>` and close with ``, so: + +```tsx +element = this is in a fragment +// is the same as: +element = <>this is in a fragment +``` + +As a little extra part of this, try to figure out why `` is +needed at all. Might help if you look at what this looks like using +`React.createElement` rather than JSX. As I said, understanding how JXS compiles +to `React.createElement` really helps improve your capabilities with JSX! + ## 🦉 Feedback Fill out diff --git a/src/exercise/04.extra-4.html b/src/exercise/04.extra-4.html new file mode 100644 index 000000000..b7b1e9814 --- /dev/null +++ b/src/exercise/04.extra-4.html @@ -0,0 +1,41 @@ + + + + + +
+ + + + + diff --git a/src/exercise/04.md b/src/exercise/04.md index dab9ce7d5..617486104 100644 --- a/src/exercise/04.md +++ b/src/exercise/04.md @@ -10,8 +10,13 @@ Just like in regular JavaScript, you often want to share code which you do using functions. If you want to share JSX, you can do that as well. In React we call these functions "components" and they have some special properties. -Components are basically functions which return something that is "renderable" -(more React elements, strings, `null`, numbers, etc.) +Components are functions which accept an object called "props" and return +something that is renderable (more React elements, strings, `null`, numbers, +etc.). To be clear, this is _the_ definition of a React component. That's all it +is. So I'll say it again: + +> Components are functions which accept an object called "props" and return +> something that is renderable ## Exercise @@ -58,9 +63,9 @@ to understand them. We'll get to custom components in the extra credit. [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-1.html) -So far we've only used `React.createElement(someString)`, but the first argument -to `React.createElement` can also be a function which returns something that's -renderable. +So far we've only used `React.createElement('someString')`, but the first +argument to `React.createElement` can also be a function which returns something +that's renderable. So instead of calling your `message` function, pass it as the first argument to `React.createElement` and pass the `{children: 'Hello World'}` object as the @@ -70,114 +75,56 @@ second argument. [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-2.html) -We're so close! Just like using JSX for regular `div`s is nicer than using the -raw `React.createElement` API, using JSX for custom components is nicer too. -Remember that it's Babel that's responsible for taking our JSX and compiling it -to `React.createElement` calls so we just need a way to tell Babel how to -compile our JSX so it passes the function by its name rather than a string. - -We do this by how the JSX appears. Here are a few examples of Babel output for -JSX: - -```javascript -ui = // React.createElement(Capitalized) -ui = // React.createElement(property.access) -ui = // React.createElement(Property.Access) -ui = // SyntaxError -ui = // React.createElement('lowercase') -ui = // React.createElement('kebab-case') -ui = // React.createElement('Upper-Kebab-Case') -ui = // React.createElement(Upper_Snake_Case) -ui = // React.createElement('lower_snake_case') -``` - -See if you can change your component function name so people can use it with JSX -more easily! - -### 3. 💯 Runtime validation with PropTypes - -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-3.html) - -Let's change the Message component a little bit. Make it look like this now: +Rather than: -```javascript -function Message({subject, greeting}) { - return ( -
- {greeting}, {subject} -
- ) -} +```tsx +element = React.createElement(message, {children: 'Hello World'}) ``` -So now we'll use it like this: +I want to use JSX, even for custom components, like this: -```javascript - - +```tsx +element = Hello World ``` -What happens if I forget to pass the `greeting` or `subject` props? It's not -going to render properly. We'll end up with a dangling comma somewhere. It would -be nice if we got some sort of indication that we passed the wrong value to the -component. This is what the `propTypes` feature is for. Here's an example of how -you use `propTypes`: - -```javascript -function FavoriteNumber({favoriteNumber}) { - return
My favorite number is: {favoriteNumber}
-} - -const PropTypes = { - number(props, propName, componentName) { - if (typeof props[propName] !== 'number') { - return new Error('Some useful error message here') - } - }, -} - -FavoriteNumber.propTypes = { - favoriteNumber: PropTypes.number, -} -``` - -With that, if I do this: - -```javascript - -``` - -I'll get an error in the console. - -For this extra credit, add `propTypes` support to your updated component -(remember to update it to have the subject and greeting). - -🦉 Note that prop types add some runtime overhead resulting in sub-optimal -performance, so they are not run in production. - -📜 Read more about prop-types: - -- https://reactjs.org/docs/typechecking-with-proptypes.html +And we're so close! Just like using JSX for regular `div`s is nicer than using +the raw `React.createElement` API, using JSX for custom components is nicer too. +Remember that it's Babel that's responsible for taking our JSX and compiling it +to `React.createElement` calls. If we try `Hello World, +here's what Babel will do: -### 4. 💯 Use the prop-types package +```tsx +element = Hello World -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-4.html) +// the desired output +element = React.createElement(message, {children: 'Hello World'}) -As it turns out, there are some pretty common things you'd want to validate, so -the React team maintains a package of these called -[`prop-types`](https://npm.im/prop-types). Go ahead and get that added to the -page by adding a script tag for it: +// the actual output +element = React.createElement('message', {children: 'Hello World'}) +``` -```html - +So we just need a way to tell Babel how to compile our JSX so it passes the +function by its name rather than a string. We do this by how the JSX appears. +Here are a few examples of Babel output for JSX: + +```tsx +element = // React.createElement(Capitalized) +element = // React.createElement(property.access) +element = // React.createElement(Property.Access) +element = // SyntaxError +element = // React.createElement('lowercase') +element = // React.createElement('kebab-case') +element = // React.createElement('Upper-Kebab-Case') +element = // React.createElement(Upper_Snake_Case) +element = // React.createElement('lower_snake_case') ``` -Then use that package instead of writing it yourself. Also, make use of the -`isRequired` feature! +Now let's refactor your function to a name that will make it possible to call it +by using it as a JSX component. -### 5. 💯 using React Fragments +### 3. 💯 using React Fragments -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-5.html) +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-3.html) One feature of JSX that you'll find useful is called ["React Fragments"](https://reactjs.org/docs/fragments.html). It's a special @@ -188,7 +135,51 @@ The component is available via ``. Replace the `
` with a fragment and inspect the DOM to notice that the elements are both rendered as direct children of `root`. -## 🦉 Feedback +### 4. 💯 Custom Props + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-4.html) + +You are the commander of your component's API. The API for your component is +"props" which is the object your component function accepts as an argument. For +example, our `Message` component uses the "special" and implicit `children` +prop. It's special because it means we can do this: + +```tsx +element = Hello World +// is functionally equivalent to +element = + +// and this: +element = ( + + Hello World + +) +// is functionally equivalent to +element = Hello, ' ', World]} /> +``` + +But we don't have to use the `children` prop, we can call it whatever we want. +And sometimes using something other than the `children` prop can be really +useful. For example, let's imagine a `Calculator` component that can display an +equation and it's solution, like so: + +```tsx +element = +// should render: +//
+// +// 1 + 2 = 3 +// +//
+``` + +Let's get some more practice with custom components. + +This extra credit primes us for the next exercise which is a different example, +so you'll find starter code for this extra credit in `exercise/04.extra-4.html`. + +## 🦉 Elaboration and Feedback Fill out [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=04%3A%20Creating%20custom%20components&em=). diff --git a/src/exercise/05.md b/src/exercise/05.md index b832d5745..5154266a8 100644 --- a/src/exercise/05.md +++ b/src/exercise/05.md @@ -1,4 +1,4 @@ -# Styling +# TypeScript with React ## 📝 Your Notes @@ -6,125 +6,318 @@ Elaborate on your learnings here in `src/exercise/05.md` ## Background -There are two primary ways to style react components +TypeScript is an enormously valuable team productivity, code quality, and +confidence tool that I strongly recommend you use to build any React application +you plan making maintainable. -1. Inline styles with the `style` prop -2. Regular CSS with the `className` prop +Remember, in exercise 4 we learned what a React component is: -**About the `style` prop:** +> Components are functions which accept an object called "props" and return +> something that is renderable -- In HTML you'd pass a string of CSS: +Because of this, they don't require any special considerations when applying +TypeScript type annotations to them. You treat React component functions the +same way you treat regular functions. Because of this, the major battle for +folks using TypeScript with React is: -```html -
+1. Improving their TypeScript skills +2. Learning the React-specific types available + +📜 I advise having the +[React+TypeScript Cheatsheets](https://github.com/typescript-cheatsheets/react) +repo open as a reference while you're getting used to using TypeScript and +React. + +With that said, here's a quick intro to adding type annotations to functions: + +```tsx +// here's a regular JS function that accepts a user +// which might have a name property +function getName(user) { + return user.name ?? 'Unknown' +} + +// here's how you'd type that user object to say it has an optional name property: +type User = {name?: string} + +// and here's how you'd tell TypeScript that the user parameter is a User object +function getName(user: User) { + return user.name ?? 'Unknown' +} + +// and if you'd like to, you can specify the return type explicitely as well +// (though it is inferred): +function getName(user: User): string { + return user.name ?? 'Unknown' +} ``` -- In React, you'll pass an object of CSS: +📜 Learn more about the syntax for functions in TypeScript here: +[TypeScript Function Syntaxes](https://kentcdodds.com/blog/typescript-function-syntaxes) + +Let's look at a quick example of adding TypeScript to a simple (and familiar) +React component: + +```tsx +function Message(props) { + return
{props.children}
+} + +// We could say: +type MessageProps = {children: string} +function Message(props: MessageProps) { + return
{props.children}
+} +// that would allow: Hello World +// but not: Hello World + +// The React types have a ReactNode, which is the recommended choice for the children prop: +type MessageProps = {children: React.ReactNode} +function Message(props: MessageProps) { + return
{props.children}
+} + +// keep in mind that you don't *have* to give your props a name. +// You can inline them as well. This works just the same as above: +function Message(props: {children: React.ReactNode}) { + return
{props.children}
+} + +// and you can destructure as well: +function Message({children}: {children: React.ReactNode}) { + return
{children}
+} -```jsx -
+// mix-and-match (this is what I do most of the time): +type MessageProps = {children: React.ReactNode} +function Message({children}: MessageProps) { + return
{children}
+} ``` -Note that in react the `{{` and `}}` is actually a combination of a JSX -expression and an object expression. The same example above could be written -like so: +📜 Learn more about typing React components here: +[How to write a React Component in TypeScript](https://kentcdodds.com/blog/how-to-write-a-react-component-in-typescript) + +🦉 It's great to have your code type checked, but not at the expense of +progressing and learning. If you get totally stuck on something, then I suggest +you tell TypeScript to quiet down and come back to it later when you're more +experienced or want to focus on it. You can do this like so: -```jsx -const myStyles = {marginTop: 20, backgroundColor: 'blue'} -
+```typescript +// @ts-expect-error TypeScript is complaining about this next line. +// Something about magic not existing. +// I don't know how to fix this right now... Come back later. +make.magic() ``` -Note also that the property names are `camelCased` rather than `kebab-cased`. -This matches the `style` property of DOM nodes (which is a -[`CSSStyleDeclaration`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) -object). +💰 Tip: If you're an experienced TypeScript developer and want to enable strict +mode, open `config/tsconfig.exercise.json` and set `strict` to `true` in there. -**About the `className` prop:** +💰 Tip: If you're not interested in using TypeScript and would rather work +through all the exercises with JavaScript, then run this in your terminal: + +``` +./scripts/remove-ts +``` -As we discussed earlier, in HTML, you apply a class name to an element with the -`class` attribute. In JSX, you use the `className` prop. +This will convert all the files to JavaScript. You may need to restart your +development server for the change to take effect. ## Exercise Production deploys: -- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/05.js) -- [Final](http://react-fundamentals.netlify.app/isolated/final/05.js) +- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/05.tsx) +- [Final](http://react-fundamentals.netlify.app/isolated/final/05.tsx) -In this exercise we'll use both methods for styling react components. +In this exercise, we're going to take the `Calculator` component we have in the +extra credit of the previous exercise and add type annotations to it. We're +moving from the `*.html` files to `*.tsx` files. This is more real-world and +also your editor likely supports TypeScript better within TypeScript files +rather than HTML files. -We have the following css on the page: +In addition to typing the function itself, we'll also be able to play around +with some approaches to typing the `operations` object to make things easier for +us as well as people using our component. -```css -.box { - border: 1px solid #333; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; -} -.box--large { - width: 270px; - height: 270px; -} -.box--medium { - width: 180px; - height: 180px; -} -.box--small { - width: 90px; - height: 90px; -} +Now, open `src/exercise/05.tsx` and follow the emoji there. You'll notice that +instead of calling `ReactDOM.render`, we're exporting an `App` component. This +is how the rest of the workshops will work. Rest assured, `ReactDOM.render` _is_ +being called for you under the hood, just like we were doing earlier. + +## Extra Credit + +### 1. 💯 improve autocomplete for the operator string + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-1.tsx) + +Our `CalculatorProps['operator']` type being set simply to `string` is not +"narrow" enough to help users of our `Calculator` component. It allows _any_ +`string` value to be provided, even one which our Calculator doesn't support. +For example, the exponentiation operator `**` could be passed and TypeScript +won't complain, but this would cause a runtime error because we don't have a +function to handle that operator: + +```tsx +element = // 💥 ``` -Your job is to apply the right className and style props to the divs below so -the styles applied match the text content. +On top of that, the API for our `Calculator` isn't very discoverable. How would +people know which `operations` are possible? Docs? Trial and error? -## Extra Credit +Rather than a `string`, your TypeScript type definition can be set to a specific +string. For example: + +```tsx +type KodyString = 'Kody' +let kody: KodyString // this variable can only ever be set to the string 'Kody' +``` + +Combine that functionality with +[union syntax of `|`](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) +and you'll be able to specify exactly which operators are allowed. For example: + +```tsx +type KodyOrHannahString = 'Kody' | 'Hannah' +let assistant: KodyOrHannahString // this variable can only ever be set to the string 'Kody' or 'Hannah' -### 1. 💯 Create a custom component +// 💰 tip: we could do the same thing without creating a type by inlining instead: +// let assistant: 'Kody' | 'Hannah' +``` + +How about we narrow our `operator` type from a `string` to some specific strings +using a union. + +### 2. 💯 derive the operator type from the operations object + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-2.tsx) -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-1.js) +You may have noticed that we're duplicating our operators of `+`, `-`, `*`, and +`/`. Any time we want to add a new operator, we have to add it in two places and +if we miss one then we could either have a runtime error, or users won't be able +to use our new operator at all. -Try to make a custom `` component that renders a div, accepts all the -props and merges the given `style` and `className` props with the shared values. +It would be better if we could have the compiler let us know we missed one +(foreshadowing... look forward to that in an upcoming extra credit) or just +derive the possible operators. -I should be able to use it like so: +To do this, you need to know about two TypeScript keywords: `typeof` and +`keyof`. Technically `typeof` is a JavaScript feature, but TypeScript builds on +top of this and will get you the TypeScript type for the given variable. So if +you say: -```jsx - - small lightblue box - +```tsx +const user = {name: 'kody', isCute: true} +type User = typeof user +// type User = { name: string; isCute: boolean; } ``` -The `box` className and `fontStyle: 'italic'` style should be applied in -addition to the values that come from props. +And then you can use `keyof` to get a union-ed type of strings of all the keys +in a given type: + +```tsx +type UserKeys = keyof User +// type UserKeys = "name" | "isCute" +``` + +📜 Learn more about TypeScript's +[`typeof` operator](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html) +and +[`keyof` operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html). + +With that, try and derive the type of the `CalculatorProps['operator']` so you +don't have to repeat yourself. -### 2. 💯 accept a size prop to encapsulate styling +### 3. 💯 default prop values -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-2.js) +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-3.tsx) -It's great that we're composing the `className`s and `style`s properly, but -wouldn't it be better if the users of our components didn't have to worry about -which class name to apply for a given effect? Or that a class name is involved -at all? I think it would be better if users of our component had a `size` prop -and our component took care of making the box that size. +Sometimes you want to allow the user of your component to skip providing a prop +and use a default value instead. To do this, we can use +[destructuring default values syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Default_values_2), +but when users of our component try to skip a prop, TypeScript will complain +because our type says all the elements of the `CalculatorProps` type are +required. -In this extra credit, try to make this API work: +So when you make a prop optional, make sure you provide any relevant default +value as well as mark it as optional using the +[optional properties syntax](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties): -```jsx - - small lightblue box - +```tsx +type User = {name: string; isCute?: boolean} +// name is required, isCute is optional, so these both compile: +const kody = {name: 'Kody', isCute: true} +const peter = {name: 'Peter'} ``` -## Attribution +For this extra credit, make all props optional. Default `left` and `right` to +`0` and `operator` to `'+'`. Then you can update the App to test it out: + +```tsx +function App() { + return ( +
+

Calculator

+ + + + +
+ ) +} +``` + +### 4. 💯 reduce duplication for operation functions + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-4.tsx) + +🦉 These last two extra credits have little to do with React and everything to +do with TypeScript. If you'd rather skip these two, I won't be offended 🥲 + +One last thing that bugs me is the repetition in the `operations` type. The type +for every one of those functions is the same. They all accept two numbers and +return a number. + +One thing we could do is extract that function into a type and then tell +TypeScript that the `operations` object is a `Record` where the key is one of +the valid operators and the value is an `OperationFn`. + +I'm going to let you try this one on your own. + +💰 But I'll give you some hints: + +- You'll need 📜 + [TypeScript's `Record` Utility Type](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype) +- You'll have to manually create a union of all allowed `operations` again for + the Record's `key` +- You'll need to define 📜 + [a function type](https://kentcdodds.com/blog/typescript-function-syntaxes) + for the Record's value + +🦉 At the end of this one, you may prefer the previous version and that's fine. +This is just two ways to do it and they both come with trade-offs. Personally, I +prefer this way to avoid typing all the functions individually. + +🦉 Also, you may wonder why we went back to repeating ourselves. Unfortunately +there's no way around it if you want to define the object as a Record with a +specific key. However! It's not as bad as before because if we make a mistake +and forget to update both places, the compiler will complain at us rather than +having a runtime error, so it's less of a problem. And there's actually a +workaround for this, which is what the next extra credit is all about! + +### 5. 💯 use a "Type Narrowing Identity Function (TNIF)" + +Ok, so repeating ourselves there is not awesome. The problem is that we want to +enforce the value of our `operations` object, but to do that we either have to +widen the type of our `key` or list it explicitly as we're doing. + +What we need is some way to enforce the values of our object, without having to +annotate our object. That's what a TNIF is. I've written a blog post to describe +this, so I'll let you go through that, and then try to make that work yourself: -[Matt Zabriskie](https://twitter.com/mzabriskie) developed this example -originally for -[a workshop we gave together.](https://github.com/mzabriskie/react-workshop) +**[How to write a type narrowing identity function (TNIF) in TypeScript](https://kentcdodds.com/blog/how-to-write-a-type-narrowing-identity-function-in-typescript)** ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=05%3A%20Styling&em=). +[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=05%3A%20TypeScript%20with%20React&em=). diff --git a/src/exercise/05.tsx b/src/exercise/05.tsx new file mode 100644 index 000000000..9bdfeb922 --- /dev/null +++ b/src/exercise/05.tsx @@ -0,0 +1,40 @@ +// TypeScript with React +// http://localhost:3000/isolated/exercise/05.js + +import * as React from 'react' + +// 🐨 add type definitions for each function +const operations = { + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '*': (left, right) => left * right, + '/': (left, right) => left / right, +} + +// 🐨 create a type called CalculatorProps + +// 🐨 set the type for this props argument to CalculatorProps +function Calculator({left, operator, right}) { + const result = operations[operator](left, right) + return ( +
+ + {left} {operator} {right} = {result} + +
+ ) +} + +function App() { + return ( +
+

Calculator

+ + + + +
+ ) +} + +export {App} diff --git a/src/exercise/06.md b/src/exercise/06.md index c76038cb5..7ed98e78e 100644 --- a/src/exercise/06.md +++ b/src/exercise/06.md @@ -1,4 +1,4 @@ -# Forms +# Styling ## 📝 Your Notes @@ -6,150 +6,125 @@ Elaborate on your learnings here in `src/exercise/06.md` ## Background -In React, there actually aren't a ton of things you have to learn to interact -with forms beyond what you can do with regular DOM APIs and JavaScript. Which I -think is pretty awesome. +There are two primary ways to style react components -You can attach a submit handler to a form element with the `onSubmit` prop. This -will be called with the submit event which has a `target`. That `target` is a -reference to the `
` DOM node which has a reference to the elements of the -form which can be used to get the values out of the form! +1. Inline styles with the `style` prop +2. Regular CSS with the `className` prop -## Exercise +**About the `style` prop:** -Production deploys: +- In HTML you'd pass a string of CSS: -- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/06.js) -- [Final](http://react-fundamentals.netlify.app/isolated/final/06.js) +```html +
+``` -In this exercise, we have a form where you can submit a username and then you'll -get an "alert" showing what you typed. +- In React, you'll pass an object of CSS: -🦉 There are several ways to get the value of the name input: +```jsx +
+``` -- Via their index: `event.target.elements[0].value` -- Via the elements object by their `name` or `id` attribute: - `event.target.elements.usernameInput.value` -- There's another that I'll save for the extra credit +Note that in react the `{{` and `}}` is actually a combination of a JSX +expression and an object expression. The same example above could be written +like so: -## Extra Credit +```jsx +const myStyles = {marginTop: 20, backgroundColor: 'blue'} +
+``` -### 1. 💯 using refs +Note also that the property names are `camelCased` rather than `kebab-cased`. +This matches the `style` property of DOM nodes (which is a +[`CSSStyleDeclaration`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) +object). -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-1.js) +**About the `className` prop:** -Another way to get the value is via a `ref` in React. A `ref` is an object that -stays consistent between renders of your React component. It has a `current` -property on it which can be updated to any value at any time. In the case of -interacting with DOM nodes, you can pass a `ref` to a React element and React -will set the `current` property to the DOM node that's rendered. +As we discussed earlier, in HTML, you apply a class name to an element with the +`class` attribute. In JSX, you use the `className` prop. -So if you create an `inputRef` object via `React.useRef`, you could access the -value via: `inputRef.current.value` -(📜https://reactjs.org/docs/hooks-reference.html#useref) +## Exercise -Try to get the usernameInput's value using a ref. +Production deploys: -### 2. 💯 Validate lower-case +- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/06.tsx) +- [Final](http://react-fundamentals.netlify.app/isolated/final/06.tsx) -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-2.js) +In this exercise we'll use both methods for styling react components. -With React, the way you use state is via a special "hook" called `useState`. -Here's a simple example of what that looks like: +We have the following css on the page: -```jsx -function Counter() { - const [count, setCount] = React.useState(0) - const increment = () => setCount(count + 1) - return +```css +.box { + border: 1px solid #333; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} +.box--large { + width: 270px; + height: 270px; +} +.box--medium { + width: 180px; + height: 180px; +} +.box--small { + width: 90px; + height: 90px; } ``` -`React.useState` accepts a default initial value and returns an array. Typically -you'll destructure that array to get the state and a state updater function. - -📜 https://reactjs.org/docs/hooks-state.html +Your job is to apply the right className and style props to the divs below so +the styles applied match the text content. -In this exercise, we're going to say that this username input only accepts -lower-case characters. So if someone types an upper-case character, that's -invalid input and we'll show an error message. - -If we want our form to be dynamic, we'll need a few things: - -1. Component state to store the dynamic values (an error message in our case) -2. A change handler on the input so we know what the value is as the user - changes it and can update the error state. +## Extra Credit -Once we have that wired up then we can render the error message and disable the -submit button if there's an error. +### 1. 💯 Create a custom component -💰 This one's a little more tricky, so here are a few things you need to do to -make this work: +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-1.tsx) -1. Create a `handleChange` function that accepts the change `event` and uses - `event.target.value` to get the value of the input. Remember this event will - be triggered on the input, not the form. -2. Use the value of the input to determine whether there's an error. There's an - error if the user typed any upper-case characters. You can check this really - easily via `const isValid = value === value.toLowerCase()` -3. If there's an error, set the error state to `'Username must be lower case'`. - (💰 here's how you do that: - `setError(isValid ? null : 'Username must be lower case')`) and disable the - submit button. -4. Finally, display the error in an element +Try to make a custom `` component that renders a div, accepts all the +props and merges the given `style` and `className` props with the shared values. -You may consider adding a `role="alert"` to the element you use to display the -error to assist with screen reader users. +I should be able to use it like so: -Make sure you pass `handleChange` to the `onChange` handler of the `input`. +```jsx + + small lightblue box + +``` -### 3. 💯 Control the input value +The `box` className and `fontStyle: 'italic'` style should be applied in +addition to the values that come from props. -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-3.js) +### 2. 💯 accept a size prop to encapsulate styling -Sometimes you have form inputs which you want to programmatically control. Maybe -you want to set their value explicitly when the user clicks a button, or maybe -you want to change what the value is as the user is typing. +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-2.tsx) -This is why React supports Controlled Form inputs. So far in our exercises, all -of the form inputs have been "uncontrolled" which means that the browser is -maintaining the state of the input by itself and we can be notified of changes -and "query" for the value from the DOM node. +It's great that we're composing the `className`s and `style`s properly, but +wouldn't it be better if the users of our components didn't have to worry about +which class name to apply for a given effect? Or that a class name is involved +at all? I think it would be better if users of our component had a `size` prop +and our component took care of making the box that size. -If we want to explicitly update that value we could do this: -`inputNode.value = 'whatever'` but that's pretty imperative. Instead, React -allows us to programmatically set the `value` prop on the input like so: +In this extra credit, try to make this API work: ```jsx - + + small lightblue box + ``` -Once we do that, React ensures that the value of that input can never differ -from the value of the `myInputValue` variable. - -Typically you'll want to provide an `onChange` handler as well so you can be -made aware of "suggested changes" to the input's value (where React is basically -saying "if I were controlling this value, here's what I would do, but you do -whatever you want with this"). - -Typically you'll want to store the input's value in a state variable (via -`React.useState`) and then the `onChange` handler will call the state updater to -keep that value up-to-date. - -Wouldn't it be even cooler if instead of showing an error message we just didn't -allow the user to enter invalid input? Yeah! In this exercise I've backed us up -and removed the error stuff and now we're going to control the input state and -control the input value. Anytime there's a change we'll call `.toLowerCase()` on -the value to ensure that it's always the lower case version of what the user -types. +## Attribution -So we can get rid of our `error` state and instead we'll manage state called -`username` (with `React.useState`) and we'll set the `username` to whatever the -input value is. We'll just lowercase the input value before doing so. Then we'll -pass that value to the `input`'s `value` prop and now it's impossible for users -to enter an invalid value! +[Matt Zabriskie](https://twitter.com/mzabriskie) developed this example +originally for +[a workshop we gave together.](https://github.com/mzabriskie/react-workshop) ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=06%3A%20Forms&em=). +[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=06%3A%20Styling&em=). diff --git a/src/exercise/05.js b/src/exercise/06.tsx similarity index 97% rename from src/exercise/05.js rename to src/exercise/06.tsx index 85bcebbea..f88bfae4c 100644 --- a/src/exercise/05.js +++ b/src/exercise/06.tsx @@ -26,4 +26,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/exercise/07.md b/src/exercise/07.md index 864bfab46..e37c7bc52 100644 --- a/src/exercise/07.md +++ b/src/exercise/07.md @@ -1,4 +1,4 @@ -# Rendering Arrays +# Forms ## 📝 Your Notes @@ -6,92 +6,150 @@ Elaborate on your learnings here in `src/exercise/07.md` ## Background -One of the more tricky things with React is the requirement of a `key` prop when -you attempt to render a list of elements. +In React, there actually aren't a ton of things you have to learn to interact +with forms beyond what you can do with regular DOM APIs and JavaScript. Which I +think is pretty awesome. -If we want to render a list like this, then there's no problem: +You can attach a submit handler to a form element with the `onSubmit` prop. This +will be called with the submit event which has a `target`. That `target` is a +reference to the `` DOM node which has a reference to the elements of the +form which can be used to get the values out of the form! -```jsx -const ui = ( -
    -
  • One
  • -
  • Two
  • -
  • Three
  • -
-) -``` +## Exercise + +Production deploys: -But rendering an array of elements is very common: +- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/07.tsx) +- [Final](http://react-fundamentals.netlify.app/isolated/final/07.tsx) -```jsx -const list = ['One', 'Two', 'Three'] - -const ui = ( -
    - {list.map(listItem => ( -
  • {listItem}
  • - ))} -
-) -``` +In this exercise, we have a form where you can submit a username and then you'll +get an "alert" showing what you typed. + +🦉 There are several ways to get the value of the name input: + +- Via their index: `event.target.elements[0].value` +- Via the elements object by their `name` or `id` attribute: + `event.target.elements.usernameInput.value` +- There's another that I'll save for the extra credit + +## Extra Credit + +### 1. 💯 using refs + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/07.extra-1.tsx) + +Another way to get the value is via a `ref` in React. A `ref` is an object that +stays consistent between renders of your React component. It has a `current` +property on it which can be updated to any value at any time. In the case of +interacting with DOM nodes, you can pass a `ref` to a React element and React +will set the `current` property to the DOM node that's rendered. + +So if you create an `inputRef` object via `React.useRef`, you could access the +value via: `inputRef.current.value` +(📜https://reactjs.org/docs/hooks-reference.html#useref) + +Try to get the usernameInput's value using a ref. -Those will generate the same HTML, but what it actually does is slightly -different. Let's re-write it to see that difference: +### 2. 💯 Validate lower-case + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/07.extra-2.tsx) + +With React, the way you use state is via a special "hook" called `useState`. +Here's a simple example of what that looks like: ```jsx -const list = ['One', 'Two', 'Three'] -const listUI = list.map(listItem =>
  • {listItem}
  • ) -// notice that listUI is an array -const ui =
      {listUI}
    +function Counter() { + const [count, setCount] = React.useState(0) + const increment = () => setCount(count + 1) + return +} ``` -So we're interpolating an array of renderable elements. This is totally -acceptable, but it has interesting implications for when things change over -time. +`React.useState` accepts a default initial value and returns an array. Typically +you'll destructure that array to get the state and a state updater function. -If you re-render that list with an added item, React doesn't really know whether -you added an item in the middle, beginning, or end. And the same goes for when -you remove an item (it doesn't know whether that happened in the middle, -beginning, or end either). +📜 https://reactjs.org/docs/hooks-state.html -In this example, it's not a big deal, because React's best-guess is right and it -works out ok. However, if any of those React elements represent a component that -is maintaining state, that can be pretty problematic, which this exercise -demonstrates. +In this exercise, we're going to say that this username input only accepts +lower-case characters. So if someone types an upper-case character, that's +invalid input and we'll show an error message. -## Exercise +If we want our form to be dynamic, we'll need a few things: -Production deploys: +1. Component state to store the dynamic values (an error message in our case) +2. A change handler on the input so we know what the value is as the user + changes it and can update the error state. -- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/07.js) -- [Final](http://react-fundamentals.netlify.app/isolated/final/07.js) +Once we have that wired up then we can render the error message and disable the +submit button if there's an error. -In this exercise, we have a list of fruit that appear and can be removed. There -is state that exists (managed by the browser) in the `` for each of the -fruit. Without a key, React has no way of knowing which React element you return -the second time corresponds to the specific DOM nodes it removes, so it does its -best. For all React knows, you removed an input and gave another label different -text content, which leads to the bug we'll see in the exercise. +💰 This one's a little more tricky, so here are a few things you need to do to +make this work: -If you remove them from bottom to top then things work fine, but if you remove -them in any other order you'll notice that the wrong inputs are getting removed. +1. Create a `handleChange` function that accepts the change `event` and uses + `event.target.value` to get the value of the input. Remember this event will + be triggered on the input, not the form. +2. Use the value of the input to determine whether there's an error. There's an + error if the user typed any upper-case characters. You can check this really + easily via `const isValid = value === value.toLowerCase()` +3. If there's an error, set the error state to `'Username must be lower case'`. + (💰 here's how you do that: + `setError(isValid ? null : 'Username must be lower case')`) and disable the + submit button. +4. Finally, display the error in an element -This one is more of a demo than an exercise because the actual solution is -pretty simple. Just do what the emoji says :) +You may consider adding a `role="alert"` to the element you use to display the +error to assist with screen reader users. -## Extra Credit +Make sure you pass `handleChange` to the `onChange` handler of the `input`. + +### 3. 💯 Control the input value + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/07.extra-3.tsx) + +Sometimes you have form inputs which you want to programmatically control. Maybe +you want to set their value explicitly when the user clicks a button, or maybe +you want to change what the value is as the user is typing. + +This is why React supports Controlled Form inputs. So far in our exercises, all +of the form inputs have been "uncontrolled" which means that the browser is +maintaining the state of the input by itself and we can be notified of changes +and "query" for the value from the DOM node. + +If we want to explicitly update that value we could do this: +`inputNode.value = 'whatever'` but that's pretty imperative. Instead, React +allows us to programmatically set the `value` prop on the input like so: + +```jsx + +``` + +Once we do that, React ensures that the value of that input can never differ +from the value of the `myInputValue` variable. + +Typically you'll want to provide an `onChange` handler as well so you can be +made aware of "suggested changes" to the input's value (where React is basically +saying "if I were controlling this value, here's what I would do, but you do +whatever you want with this"). -### 1. 💯 Focus Demo +Typically you'll want to store the input's value in a state variable (via +`React.useState`) and then the `onChange` handler will call the state updater to +keep that value up-to-date. -[Production deploy](http://react-fundamentals.netlify.app/isolated/final/07.extra-1.js) +Wouldn't it be even cooler if instead of showing an error message we just didn't +allow the user to enter invalid input? Yeah! In this exercise I've backed us up +and removed the error stuff and now we're going to control the input state and +control the input value. Anytime there's a change we'll call `.toLowerCase()` on +the value to ensure that it's always the lower case version of what the user +types. -This extra credit is 100% demo. You can observe that state also includes -keyboard focus as well as selection! You'll also notice that using the array -`index` as a key is no different from React's default behavior, so it's unlikely -to fix issues if you're having them. Best to use a unique ID. Play around with -it! +So we can get rid of our `error` state and instead we'll manage state called +`username` (with `React.useState`) and we'll set the `username` to whatever the +input value is. We'll just lowercase the input value before doing so. Then we'll +pass that value to the `input`'s `value` prop and now it's impossible for users +to enter an invalid value! ## 🦉 Feedback Fill out -[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=07%3A%20Rendering%20Arrays&em=). +[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=07%3A%20Forms&em=). diff --git a/src/exercise/06.js b/src/exercise/07.tsx similarity index 83% rename from src/exercise/06.js rename to src/exercise/07.tsx index c8d3ac1c2..b91417405 100644 --- a/src/exercise/06.js +++ b/src/exercise/07.tsx @@ -3,7 +3,11 @@ import * as React from 'react' -function UsernameForm({onSubmitUsername}) { +function UsernameForm({ + onSubmitUsername, +}: { + onSubmitUsername: (username: string) => void +}) { // 🐨 add a submit event handler here (`handleSubmit`). // 💰 Make sure to accept the `event` as an argument and call // `event.preventDefault()` to prevent the default behavior of form submit @@ -30,8 +34,9 @@ function UsernameForm({onSubmitUsername}) { } function App() { - const onSubmitUsername = username => alert(`You entered: ${username}`) + const onSubmitUsername = (username: string) => + alert(`You entered: ${username}`) return } -export default App +export {App} diff --git a/src/exercise/08.md b/src/exercise/08.md new file mode 100644 index 000000000..6c541e955 --- /dev/null +++ b/src/exercise/08.md @@ -0,0 +1,97 @@ +# Rendering Arrays + +## 📝 Your Notes + +Elaborate on your learnings here in `src/exercise/08.md` + +## Background + +One of the more tricky things with React is the requirement of a `key` prop when +you attempt to render a list of elements. + +If we want to render a list like this, then there's no problem: + +```jsx +const element = ( +
      +
    • One
    • +
    • Two
    • +
    • Three
    • +
    +) +``` + +But rendering an array of elements is very common: + +```jsx +const list = ['One', 'Two', 'Three'] + +const element = ( +
      + {list.map(listItem => ( +
    • {listItem}
    • + ))} +
    +) +``` + +Those will generate the same HTML, but what it actually does is slightly +different. Let's re-write it to see that difference: + +```jsx +const list = ['One', 'Two', 'Three'] +const listelement = list.map(listItem =>
  • {listItem}
  • ) +// notice that listUI is an array +const element =
      {listUI}
    +``` + +So we're interpolating an array of renderable elements. This is totally +acceptable, but it has interesting implications for when things change over +time. + +If you re-render that list with an added item, React doesn't really know whether +you added an item in the middle, beginning, or end. And the same goes for when +you remove an item (it doesn't know whether that happened in the middle, +beginning, or end either). + +In this example, it's not a big deal, because React's best-guess is right and it +works out ok. However, if any of those React elements represent a component that +is maintaining state, that can be pretty problematic, which this exercise +demonstrates. + +## Exercise + +Production deploys: + +- [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/08.tsx) +- [Final](http://react-fundamentals.netlify.app/isolated/final/08.tsx) + +In this exercise, we have a list of fruit that appear and can be removed. There +is state that exists (managed by the browser) in the `` for each of the +fruit. Without a key, React has no way of knowing which React element you return +the second time corresponds to the specific DOM nodes it removes, so it does its +best. For all React knows, you removed an input and gave another label different +text content, which leads to the bug we'll see in the exercise. + +If you remove them from bottom to top then things work fine, but if you remove +them in any other order you'll notice that the wrong inputs are getting removed. + +This one is more of a demo than an exercise because the actual solution is +pretty simple. Just do what the emoji says :) + +## Extra Credit + +### 1. 💯 Focus Demo + +[Production deploy](http://react-fundamentals.netlify.app/isolated/final/08.extra-1.tsx) + +This extra credit is 100% demo. You can observe that state also includes +keyboard focus as well as selection! You'll also notice that using the array +`index` as a key is no different from React's default behavior, so it's unlikely +to fix issues if you're having them. Best to use a unique ID. Play around with +it! + +## 🦉 Feedback + +Fill out +[the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=08%3A%20Rendering%20Arrays&em=). diff --git a/src/exercise/07.js b/src/exercise/08.tsx similarity index 71% rename from src/exercise/07.js rename to src/exercise/08.tsx index 700693146..773a74118 100644 --- a/src/exercise/07.js +++ b/src/exercise/08.tsx @@ -3,24 +3,34 @@ import * as React from 'react' -const allItems = [ +type Item = {id: string; value: string} + +const allItems: Array = [ {id: 'apple', value: '🍎 apple'}, {id: 'orange', value: '🍊 orange'}, {id: 'grape', value: '🍇 grape'}, {id: 'pear', value: '🍐 pear'}, ] +function typedBoolean( + value: T, +): value is Exclude { + return Boolean(value) +} + function App() { const [items, setItems] = React.useState(allItems) function addItem() { - setItems([ - ...items, - allItems.find(i => !items.map(({id}) => id).includes(i.id)), - ]) + setItems( + [ + ...items, + allItems.find(i => !items.map(({id}) => id).includes(i.id)), + ].filter(typedBoolean), + ) } - function removeItem(item) { + function removeItem(item: Item) { setItems(items.filter(i => i.id !== item.id)) } @@ -43,4 +53,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/final/01.extra-1.html b/src/final/01.extra-1.html index cfb46b59d..59cc60435 100644 --- a/src/final/01.extra-1.html +++ b/src/final/01.extra-1.html @@ -2,14 +2,18 @@ - - - + + + + + diff --git a/src/final/01.html b/src/final/01.html index 32839c597..8292d2e49 100644 --- a/src/final/01.html +++ b/src/final/01.html @@ -1,13 +1,17 @@ - -
    - - + + +
    + + + diff --git a/src/final/02.extra-1.html b/src/final/02.extra-1.html index 712820c32..166c69507 100644 --- a/src/final/02.extra-1.html +++ b/src/final/02.extra-1.html @@ -8,14 +8,12 @@ diff --git a/src/final/02.extra-2.html b/src/final/02.extra-2.html new file mode 100644 index 000000000..100566f7c --- /dev/null +++ b/src/final/02.extra-2.html @@ -0,0 +1,24 @@ + + + + + +
    + + + + diff --git a/src/final/03.extra-3.html b/src/final/03.extra-3.html new file mode 100644 index 000000000..7bd926068 --- /dev/null +++ b/src/final/03.extra-3.html @@ -0,0 +1,22 @@ + + + + + +
    + + + + diff --git a/src/final/03.extra-4.html b/src/final/03.extra-4.html new file mode 100644 index 000000000..2c1abb1c2 --- /dev/null +++ b/src/final/03.extra-4.html @@ -0,0 +1,22 @@ + + + + + +
    + + + + diff --git a/src/final/04.extra-3.html b/src/final/04.extra-3.html index 03fc1df3b..43f8300fb 100644 --- a/src/final/04.extra-3.html +++ b/src/final/04.extra-3.html @@ -1,5 +1,5 @@ - + @@ -8,36 +8,16 @@ - - - - - - diff --git a/src/final/05.extra-1.tsx b/src/final/05.extra-1.tsx new file mode 100644 index 000000000..c4bcd8bf4 --- /dev/null +++ b/src/final/05.extra-1.tsx @@ -0,0 +1,41 @@ +// TypeScript with React +// 💯 improve autocomplete for the operator string +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +const operations = { + '+': (left: number, right: number): number => left + right, + '-': (left: number, right: number): number => left - right, + '*': (left: number, right: number): number => left * right, + '/': (left: number, right: number): number => left / right, +} + +type CalculatorProps = { + left: number + operator: '+' | '-' | '*' | '/' + right: number +} +function Calculator({left, operator, right}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.extra-2.tsx b/src/final/05.extra-2.tsx new file mode 100644 index 000000000..0215cfe91 --- /dev/null +++ b/src/final/05.extra-2.tsx @@ -0,0 +1,41 @@ +// TypeScript with React +// 💯 derive the operator type from the operations object +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +const operations = { + '+': (left: number, right: number): number => left + right, + '-': (left: number, right: number): number => left - right, + '*': (left: number, right: number): number => left * right, + '/': (left: number, right: number): number => left / right, +} + +type CalculatorProps = { + left: number + operator: keyof typeof operations + right: number +} +function Calculator({left, operator, right}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.extra-3.tsx b/src/final/05.extra-3.tsx new file mode 100644 index 000000000..50b17701d --- /dev/null +++ b/src/final/05.extra-3.tsx @@ -0,0 +1,41 @@ +// TypeScript with React +// 💯 default prop values +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +const operations = { + '+': (left: number, right: number): number => left + right, + '-': (left: number, right: number): number => left - right, + '*': (left: number, right: number): number => left * right, + '/': (left: number, right: number): number => left / right, +} + +type CalculatorProps = { + left?: number + operator?: keyof typeof operations + right?: number +} +function Calculator({left = 0, operator = '+', right = 0}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.extra-4.tsx b/src/final/05.extra-4.tsx new file mode 100644 index 000000000..eb0bae6bf --- /dev/null +++ b/src/final/05.extra-4.tsx @@ -0,0 +1,43 @@ +// TypeScript with React +// 💯 reduce duplication for operation functions +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +type OperationFn = (left: number, right: number) => number +type Operator = '+' | '-' | '/' | '*' +const operations: Record = { + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '*': (left, right) => left * right, + '/': (left, right) => left / right, +} + +type CalculatorProps = { + left?: number + operator?: keyof typeof operations + right?: number +} +function Calculator({left = 0, operator = '+', right = 0}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.extra-5.tsx b/src/final/05.extra-5.tsx new file mode 100644 index 000000000..d074b2376 --- /dev/null +++ b/src/final/05.extra-5.tsx @@ -0,0 +1,47 @@ +// TypeScript with React +// 💯 use a "Type Narrowing Identity Function (TNIF)" +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +type OperationFn = (left: number, right: number) => number + +const createOperations = >( + opts: OperationsType, +) => opts + +const operations = createOperations({ + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '*': (left, right) => left * right, + '/': (left, right) => left / right, +}) + +type CalculatorProps = { + left?: number + operator?: keyof typeof operations + right?: number +} +function Calculator({left = 0, operator = '+', right = 0}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.tsx b/src/final/05.tsx new file mode 100644 index 000000000..a71c9cf30 --- /dev/null +++ b/src/final/05.tsx @@ -0,0 +1,40 @@ +// TypeScript with React +// http://localhost:3000/isolated/final/05.js +import * as React from 'react' + +const operations = { + '+': (left: number, right: number): number => left + right, + '-': (left: number, right: number): number => left - right, + '*': (left: number, right: number): number => left * right, + '/': (left: number, right: number): number => left / right, +} + +type CalculatorProps = { + left: number + operator: string + right: number +} +function Calculator({left, operator, right}: CalculatorProps) { + const result = operations[operator](left, right) + return ( +
    + + {left} {operator} {right} = {result} + +
    + ) +} + +function App() { + return ( +
    +

    Calculator

    + + + + +
    + ) +} + +export {App} diff --git a/src/final/05.extra-1.js b/src/final/06.extra-1.tsx similarity index 85% rename from src/final/05.extra-1.js rename to src/final/06.extra-1.tsx index 7de8f0744..55edd6177 100644 --- a/src/final/05.extra-1.js +++ b/src/final/06.extra-1.tsx @@ -5,7 +5,11 @@ import * as React from 'react' import '../box-styles.css' -function Box({style, className = '', ...otherProps}) { +function Box({ + style = {}, + className = '', + ...otherProps +}: React.HTMLAttributes) { return (
    & { + size?: 'small' | 'medium' | 'large' +}) { const sizeClassName = size ? `box--${size}` : '' return (
    -
    - - -
    - - - ) -} - -function App() { - const onSubmitUsername = username => alert(`You entered: ${username}`) - return -} - -export default App diff --git a/src/final/05.js b/src/final/06.tsx similarity index 92% rename from src/final/05.js rename to src/final/06.tsx index 03ae49384..e945ddd4b 100644 --- a/src/final/05.js +++ b/src/final/06.tsx @@ -1,7 +1,6 @@ // Styling // http://localhost:3000/isolated/final/05.js -import * as React from 'react' import '../box-styles.css' const smallBox = ( @@ -39,4 +38,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/final/06.extra-1.js b/src/final/07.extra-1.tsx similarity index 53% rename from src/final/06.extra-1.js rename to src/final/07.extra-1.tsx index 087869a77..25d2e98c7 100644 --- a/src/final/06.extra-1.js +++ b/src/final/07.extra-1.tsx @@ -4,12 +4,18 @@ import * as React from 'react' -function UsernameForm({onSubmitUsername}) { - const usernameInputRef = React.useRef() +function UsernameForm({ + onSubmitUsername, +}: { + onSubmitUsername: (username: string) => void +}) { + const usernameInputRef = React.useRef(null) - function handleSubmit(event) { + function handleSubmit(event: React.SyntheticEvent) { event.preventDefault() - onSubmitUsername(usernameInputRef.current.value) + if (usernameInputRef.current) { + onSubmitUsername(usernameInputRef.current.value) + } } return ( @@ -24,8 +30,9 @@ function UsernameForm({onSubmitUsername}) { } function App() { - const onSubmitUsername = username => alert(`You entered: ${username}`) + const onSubmitUsername = (username: string) => + alert(`You entered: ${username}`) return } -export default App +export {App} diff --git a/src/final/06.extra-2.js b/src/final/07.extra-2.tsx similarity index 53% rename from src/final/06.extra-2.js rename to src/final/07.extra-2.tsx index bc8c2d5e1..f3a056a7f 100644 --- a/src/final/06.extra-2.js +++ b/src/final/07.extra-2.tsx @@ -4,16 +4,27 @@ import * as React from 'react' -function UsernameForm({onSubmitUsername}) { - const [error, setError] = React.useState(null) +interface FormElements extends HTMLFormControlsCollection { + usernameInput: HTMLInputElement +} +interface UsernameFormElement extends HTMLFormElement { + readonly elements: FormElements +} + +function UsernameForm({ + onSubmitUsername, +}: { + onSubmitUsername: (username: string) => void +}) { + const [error, setError] = React.useState(null) - function handleSubmit(event) { + function handleSubmit(event: React.SyntheticEvent) { event.preventDefault() - onSubmitUsername(event.target.elements.usernameInput.value) + onSubmitUsername(event.currentTarget.elements.usernameInput.value) } - function handleChange(event) { - const {value} = event.target + function handleChange(event: React.SyntheticEvent) { + const {value} = event.currentTarget const isLowerCase = value === value.toLowerCase() setError(isLowerCase ? null : 'Username must be lower case') } @@ -35,7 +46,8 @@ function UsernameForm({onSubmitUsername}) { } function App() { - const onSubmitUsername = username => alert(`You entered: ${username}`) + const onSubmitUsername = (username: string) => + alert(`You entered: ${username}`) return (
    @@ -43,4 +55,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/final/06.extra-3.js b/src/final/07.extra-3.tsx similarity index 50% rename from src/final/06.extra-3.js rename to src/final/07.extra-3.tsx index 38a965dfc..b27154717 100644 --- a/src/final/06.extra-3.js +++ b/src/final/07.extra-3.tsx @@ -4,16 +4,23 @@ import * as React from 'react' -function UsernameForm({onSubmitUsername}) { +interface FormElements extends HTMLFormControlsCollection { + usernameInput: HTMLInputElement +} +interface UsernameFormElement extends HTMLFormElement { + readonly elements: FormElements +} + +function UsernameForm({ + onSubmitUsername, +}: { + onSubmitUsername: (username: string) => void +}) { const [username, setUsername] = React.useState('') - function handleSubmit(event) { + function handleSubmit(event: React.SyntheticEvent) { event.preventDefault() - onSubmitUsername(username) - } - - function handleChange(event) { - setUsername(event.target.value.toLowerCase()) + onSubmitUsername(event.currentTarget.elements.usernameInput.value) } return ( @@ -23,7 +30,9 @@ function UsernameForm({onSubmitUsername}) { + setUsername(event.currentTarget.value.toLowerCase()) + } value={username} />
    @@ -33,7 +42,8 @@ function UsernameForm({onSubmitUsername}) { } function App() { - const onSubmitUsername = username => alert(`You entered: ${username}`) + const onSubmitUsername = (username: string) => + alert(`You entered: ${username}`) return (
    @@ -41,4 +51,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/final/07.tsx b/src/final/07.tsx new file mode 100644 index 000000000..efc6ad731 --- /dev/null +++ b/src/final/07.tsx @@ -0,0 +1,40 @@ +// Basic Forms +// http://localhost:3000/isolated/final/06.js + +import * as React from 'react' + +interface FormElements extends HTMLFormControlsCollection { + usernameInput: HTMLInputElement +} +interface UsernameFormElement extends HTMLFormElement { + readonly elements: FormElements +} + +function UsernameForm({ + onSubmitUsername, +}: { + onSubmitUsername: (username: string) => void +}) { + function handleSubmit(event: React.SyntheticEvent) { + event.preventDefault() + onSubmitUsername(event.currentTarget.elements.usernameInput.value) + } + + return ( +
    +
    + + +
    + +
    + ) +} + +function App() { + const onSubmitUsername = (username: string) => + alert(`You entered: ${username}`) + return +} + +export {App} diff --git a/src/final/07.extra-1.js b/src/final/08.extra-1.tsx similarity index 85% rename from src/final/07.extra-1.js rename to src/final/08.extra-1.tsx index 8f9c919ed..cddf465db 100644 --- a/src/final/07.extra-1.js +++ b/src/final/08.extra-1.tsx @@ -4,8 +4,10 @@ import * as React from 'react' +type Item = {id: string; value: string} + function FocusDemo() { - const [items, setItems] = React.useState([ + const [items, setItems] = React.useState>([ {id: 'apple', value: '🍎 apple'}, {id: 'orange', value: '🍊 orange'}, {id: 'grape', value: '🍇 grape'}, @@ -17,9 +19,9 @@ function FocusDemo() { return () => clearInterval(id) }, []) - function getChangeHandler(item) { - return event => { - const newValue = event.target.value + function getChangeHandler(item: Item) { + return (event: React.SyntheticEvent) => { + const newValue = event.currentTarget.value setItems(allItems => allItems.map(i => ({ ...i, @@ -67,7 +69,7 @@ function FocusDemo() { ) } -function shuffle(originalArray) { +function shuffle>(originalArray: ArrayType) { const array = [...originalArray] let currentIndex = array.length let temporaryValue @@ -89,4 +91,4 @@ function App() { return } -export default App +export {App} diff --git a/src/final/07.js b/src/final/08.tsx similarity index 70% rename from src/final/07.js rename to src/final/08.tsx index 1374aebec..2c061682f 100644 --- a/src/final/07.js +++ b/src/final/08.tsx @@ -3,24 +3,34 @@ import * as React from 'react' -const allItems = [ +type Item = {id: string; value: string} + +const allItems: Array = [ {id: 'apple', value: '🍎 apple'}, {id: 'orange', value: '🍊 orange'}, {id: 'grape', value: '🍇 grape'}, {id: 'pear', value: '🍐 pear'}, ] +function typedBoolean( + value: T, +): value is Exclude { + return Boolean(value) +} + function App() { const [items, setItems] = React.useState(allItems) function addItem() { - setItems([ - ...items, - allItems.find(i => !items.map(({id}) => id).includes(i.id)), - ]) + setItems( + [ + ...items, + allItems.find(i => !items.map(({id}) => id).includes(i.id)), + ].filter(typedBoolean), + ) } - function removeItem(item) { + function removeItem(item: Item) { setItems(items.filter(i => i.id !== item.id)) } @@ -42,4 +52,4 @@ function App() { ) } -export default App +export {App} diff --git a/src/index.js b/src/index.tsx similarity index 100% rename from src/index.js rename to src/index.tsx diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/setupTests.js b/src/setupTests.ts similarity index 100% rename from src/setupTests.js rename to src/setupTests.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..c0e87b875 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} From 84eb9ed2f10a060898d115e768df77c8dc603d46 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 16 Mar 2021 16:02:10 -0600 Subject: [PATCH 2/3] use ts references config --- config/tsconfig.exercise.json | 7 +++++++ config/tsconfig.final.json | 9 +++++++++ config/tsconfig.shared.json | 19 +++++++++++++++++++ package.json | 2 +- src/final/05.tsx | 1 + tsconfig.json | 23 +++++------------------ 6 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 config/tsconfig.exercise.json create mode 100644 config/tsconfig.final.json create mode 100644 config/tsconfig.shared.json diff --git a/config/tsconfig.exercise.json b/config/tsconfig.exercise.json new file mode 100644 index 000000000..9ccbb3c97 --- /dev/null +++ b/config/tsconfig.exercise.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.shared.json", + "include": ["../src/exercise/"], + "compilerOptions": { + "strict": false + } +} diff --git a/config/tsconfig.final.json b/config/tsconfig.final.json new file mode 100644 index 000000000..97df133bc --- /dev/null +++ b/config/tsconfig.final.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.shared.json", + "exclude": ["../src/exercise/"], + "include": ["../src"], + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/config/tsconfig.shared.json b/config/tsconfig.shared.json new file mode 100644 index 000000000..3122ef39e --- /dev/null +++ b/config/tsconfig.shared.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noFallthroughCasesInSwitch": true + } +} diff --git a/package.json b/package.json index bc1136b9b..8ca75ac79 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "setup": "node setup", "lint": "eslint .", "format": "prettier --write \"./src\"", - "typecheck": "tsc", + "typecheck": "tsc -b", "validate": "npm-run-all --parallel build test:coverage lint typecheck" }, "husky": { diff --git a/src/final/05.tsx b/src/final/05.tsx index a71c9cf30..e266a9baa 100644 --- a/src/final/05.tsx +++ b/src/final/05.tsx @@ -15,6 +15,7 @@ type CalculatorProps = { right: number } function Calculator({left, operator, right}: CalculatorProps) { + // @ts-expect-error we'll fix this in the next extra credit const result = operations[operator](left, right) return (
    diff --git a/tsconfig.json b/tsconfig.json index c0e87b875..82ba4a0a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,7 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": false, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] + "files": [], + "references": [ + {"path": "config/tsconfig.exercise.json"}, + {"path": "config/tsconfig.final.json"} + ] } From e266ea6a2e6fec4d622f4ae7896cb05e2ab710b1 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 16 Mar 2021 16:02:48 -0600 Subject: [PATCH 3/3] post npm run start --- tsconfig.json | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 82ba4a0a6..3e42292af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,35 @@ { "files": [], "references": [ - {"path": "config/tsconfig.exercise.json"}, - {"path": "config/tsconfig.final.json"} + { + "path": "config/tsconfig.exercise.json" + }, + { + "path": "config/tsconfig.final.json" + } + ], + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" ] }