Giter Club home page Giter Club logo

epicreact's People

Contributors

etczrn avatar

Stargazers

Roman avatar

Watchers

 avatar

epicreact's Issues

Flexible Compound Components

Flexible Compound Components

📝 Your Notes

Exercise

// Flexible Compound Components
// http://localhost:3000/isolated/exercise/03.js

import * as React from 'react'
import {Switch} from '../switch'

// 🐨 create your ToggleContext context here
// 📜 https://reactjs.org/docs/context.html#reactcreatecontext
const ToggleContext = React.createContext()

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <ToggleContext.Provider value={{on, toggle}}>
      {children}
    </ToggleContext.Provider>
  )
}

function useToggle() {
  return React.useContext(ToggleContext)
}

// 🐨 we'll still get the children from props (as it's passed to us by the
// developers using our component), but we'll get `on` implicitly from
// ToggleContext now
// 🦉 You can create a helper method to retrieve the context here. Thanks to that,
// your context won't be exposed to the user
// 💰 `const context = React.useContext(ToggleContext)`
// 📜 https://reactjs.org/docs/hooks-reference.html#usecontext
function ToggleOn({children}) {
  const {on} = useToggle()

  return on ? children : null
}

// 🐨 do the same thing to this that you did to the ToggleOn component
function ToggleOff({children}) {
  const {on} = useToggle()

  return on ? null : children
}

// 🐨 get `on` and `toggle` from the ToggleContext with `useContext`
function ToggleButton({...props}) {
  const {on, toggle} = useToggle()

  return <Switch on={on} onClick={toggle} {...props} />
}

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <div>
          <ToggleButton />
        </div>
      </Toggle>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • A common question that I get here is, should I use the children.map function ability, or should I use the context functionality? What I say is you can absolutely use the context version all the time, but using the children.map functionality might be useful if you only care about direct descendants.
  • There are use cases where you just want to enforce the fact that it only makes sense to have this be a descendant of that. You don't like to share things through context because that relationship is an important part of the API. They're use cases for both of these methods.

1. 💯 custom hook validation

// Flexible Compound Components
// http://localhost:3000/isolated/exercise/03.js

import * as React from 'react'
import {Switch} from '../switch'

// 🐨 create your ToggleContext context here
// 📜 https://reactjs.org/docs/context.html#reactcreatecontext
const ToggleContext = React.createContext()
// Tip: clarify meaning of context provider
ToggleContext.displayName = 'ToggleContext'

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <ToggleContext.Provider value={{on, toggle}}>
      {children}
    </ToggleContext.Provider>
  )
}

function useToggle() {
  const context = React.useContext(ToggleContext)

  if (!context) {
    throw new Error(`useToggle must be used within a <Toggle />`)
  }

  return React.useContext(ToggleContext)
}

function ToggleOn({children}) {
  const {on} = useToggle()

  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggle()

  return on ? null : children
}

function ToggleButton({...props}) {
  const {on, toggle} = useToggle()

  return <Switch on={on} onClick={toggle} {...props} />
}

function App() {
  return (
    // this occurs error
    // <ToggleButton />

    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <div>
          <ToggleButton />
        </div>
      </Toggle>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • ToggleContext.displayName = 'ToggleContext'
    • image
    • image

Background

One liner: The Flexible Compound Components Pattern only differs from the
previous exercise in that it uses React context. You should use this version of
the pattern more often.

Right now our component can only clone and pass props to immediate children. So
we need some way for our compound components to implicitly accept the on state
and toggle method regardless of where they're rendered within the Toggle
component's "posterity" :)

The way we do this is through context. React.createContext is the API we want.

Real World Projects that use this pattern:

Exercise

Production deploys:

The fundamental difference between this exercise and the last one is that now
we're going to allow people to render the compound components wherever they like
in the render tree. Searching through props.children for the components to
clone would be futile. So we'll use context instead.

Your job will be to make the ToggleContext which will be used to implicitly
share the state between these components, and then a custom hook to consume that
context for the compound components to do their job.

Extra Credit

1. 💯 custom hook validation

Production deploy

Change the App function to this:

const App = () => <ToggleButton />

Why doesn't that work? Can you figure out a way to give the developer a better
error message?

🦉 Feedback

Fill out
the feedback form.

Authentication

Authentication

📝 Your Notes

Exercise

/** @jsx jsx */
import {jsx} from '@emotion/core'

import {useState} from 'react'
// 🐨 you're going to need this:
import * as auth from 'auth-provider'
import {AuthenticatedApp} from './authenticated-app'
import {UnauthenticatedApp} from './unauthenticated-app'

function App() {
  // 🐨 useState for the user
  const [user, setUser] = useState(null)

  // 🐨 create a login function that calls auth.login then sets the user
  // 💰 const login = form => auth.login(form).then(u => setUser(u))
  const login = form => auth.login(form).then(user => setUser(user))

  // 🐨 create a registration function that does the same as login except for register
  const register = form => auth.register(form).then(user => setUser(user))

  // 🐨 create a logout function that calls auth.logout() and sets the user to null
  const logout = () => {
    auth.logout()
    setUser(null)
  }

  // 🐨 if there's a user, then render the AuthenticatedApp with the user and logout
  // 🐨 if there's not a user, then render the UnauthenticatedApp with login and register
  return user ? (
    <AuthenticatedApp user={user} logout={logout} />
  ) : (
    <UnauthenticatedApp login={login} register={register} />
  )
}

export {App}

/*
eslint
  no-unused-vars: "off",
*/
  • All that we did here in our app was we brought in both the authenticated side
    and the unauthenticated side of our application, and we determined which one
    of those to render based on whether we had a user in our React state.

  • Then, we brought in our authProvider here to handle the login and registration
    and logout functionalities of our application that we had already built. When
    we log in, we call auth.login. When that promise resolves, then we send the
    user state to whatever we get back from that auth.login promise. Similar for
    register, and then our logout just calls auth.logout and sets our user to null
    so that we will render the unauthenticated portion of our app.

1. 💯 Load the user's data on page load

// * app.js
import {client} from 'utils/api-client.exercise'

async function getUser() {
  let user = null
  const token = await auth.getToken()

  if (token) {
    const data = await client('me', {token})
    user = data.user
  }

  return user
}

function App() {
  const [user, setUser] = useState(null)

  useEffect(() => {
    getUser().then(user => setUser(user))
  }, [])


// * api-client.js
const apiURL = process.env.REACT_APP_API_URL

function client(
  endpoint,
  {token, headers: customHeaders, ...customConfig} = {},
) {
  const config = {
    method: 'GET',
    headers: {
      Authorization: token ? `Bearer ${token}` : undefined,
      ...customHeaders,
    },
    ...customConfig,
  }

  return window.fetch(`${apiURL}/${endpoint}`, config).then(async response => {
    const data = await response.json()
    if (response.ok) {
      return data
    } else {
      return Promise.reject(data)
    }
  })
}

export {client}
  • In review, what we've done here is we created a function called getUser that
    will try to get the user's token. If they're currently logged in, then we'll
    get a token back.

  • We can make a request to our backend to get the user's information like their
    username. Then, we assign that user property from the data we get back to the
    user and return that. Then as soon as our app is mounted, we make that
    request.

  • When that resolves, then we'll assign our user state to the user that we get
    from that getUser function. That way, the user doesn't have to log in every
    time they reach our app. To make this getUser function work, we had to add
    the functionality for client requests to be authenticated, so we had to pass
    this token.

  • To do that, we accepted that token destructured off of the custom config that
    they're providing. We attached an authorization header to our configuration.
    If the token exists, then we'll set it to bearer token.

  • Otherwise, it's undefined. We also restructured the headers so that the
    developer can provide custom headers. We can merge all that together quite
    nicely before we pass it on to window.fetch.

2. 💯 Use useAsync

/** @jsx jsx */
import {jsx} from '@emotion/core'

import {useEffect} from 'react'
import * as auth from 'auth-provider'
import {FullPageSpinner} from 'components/lib'
import * as colors from 'styles/colors'
import {client} from 'utils/api-client.exercise'
import {useAsync} from 'utils/hooks'
import {AuthenticatedApp} from './authenticated-app'
import {UnauthenticatedApp} from './unauthenticated-app'

async function getUser() {
  let user = null
  const token = await auth.getToken()

  if (token) {
    const data = await client('me', {token})
    user = data.user
  }

  return user
}

function App() {
  const {
    data: user,
    error,
    isIdle,
    isLoading,
    isError,
    isSuccess,
    run,
    setData,
  } = useAsync()

  useEffect(() => {
    run(getUser())
  }, [run])

  const login = form => auth.login(form).then(user => setData(user))

  const register = form => auth.register(form).then(user => setData(user))

  const logout = () => {
    auth.logout()
    setData(null)
  }

  if (isIdle || isLoading) {
    return <FullPageSpinner />
  }

  if (isError) {
    return (
      <div
        css={{
          color: colors.danger,
          height: '100vh',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <p>Uh oh... There's a problem. Try refreshing the app.</p>
        <pre>{error.message}</pre>
      </div>
    )
  }

  if (isSuccess) {
    return user ? (
      <AuthenticatedApp user={user} logout={logout} />
    ) : (
      <UnauthenticatedApp login={login} register={register} />
    )
  }
}

export {App}

/*
eslint
  no-unused-vars: "off",
*/

3. 💯 automatically logout on 401

import * as auth from 'auth-provider'
const apiURL = process.env.REACT_APP_API_URL

function client(
  endpoint,
  {token, headers: customHeaders, ...customConfig} = {},
) {
  const config = {
    method: 'GET',
    headers: {
      Authorization: token ? `Bearer ${token}` : undefined,
      ...customHeaders,
    },
    ...customConfig,
  }

  return window.fetch(`${apiURL}/${endpoint}`, config).then(async response => {
    const data = await response.json()

    // 401 Unauthorized
    if (response.status === 401) {
      await auth.logout()
      window.location.assign(window.location)

      return Promise.reject({message: 'Please re-authenticate'})
    }

    if (response.ok) {
      return data
    } else {
      return Promise.reject(data)
    }
  })
}

export {client}
  • In our response here, we can determine that status code with response.status.
    If that's 401, then we want to log them out. Once they're logged out, we also
    want to just clear the state of the application. We want to get the user out
    of here, and the easiest way for us to do that is to say
    window.location.assign('window.location'). That will trigger a full-page
    refresh. Any data that's in memory will just get wiped totally clean.

  • Let's also reject this promise just for completeness, even though this is
    going to trigger a refresh anyway. We'll say promise.reject. We'll just give
    it a message of "Please reauthenticate." If we save that, then we are good to
    go.

Background

Authenticated HTTP requests

Applications without user authentication cannot reliably store and present data
tied to a specific user. And users expect the ability to save data, close the
app, and return to the app and interact with the same data they created. To do
this securely (in a way that doesn't allow anyone to access anyone else's data),
you need to support authentication. The most common approach to this is a
username/password pair.

However, the user doesn't want to submit their password every time they need to
make a request for data. They want to be able to log into the application and
then the application can continuously authenticate requests for them
automatically. That said, we don't want to store the user's password and send
that with every request. A common solution to this problem is to use a special
limited use "token" which represents the user's current session. That way the
token can be invalidated (in the case that it's lost or stolen) and the user
doesn't have to change their password. They simply re-authenticate and they can
get a fresh token.

So, every request the client makes must include this token to make authenticated
requests. This is one reason it's so nice to have a small wrapper around
window.fetch: so you can automatically include this token in every request
that's made. A common way to attach the token is to use a special request header
called "Authorization".

Here's an example of how to make an authenticated request:

window.fetch('http://example.com/pets', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
})

That token can really be anything that uniquely identifies the user, but a
common standard is to use a JSON Web Token (JWT). 📜 https://jwt.io

Authentication and user identity management is a difficult problem, so it's
recommended to use a service to handle this for you. Most services will give you
a mechanism for retrieving a token when the user opens your application and you
can use that token to make authenticated requests to your backend. Some services
you might consider investigating are Auth0,
Netlify Identity,
and Firebase Authentication.

Regardless of what service provider you use (or if you build your own), the
things you'll learn in this exercise are the same:

  1. Call some API to retrieve a token
  2. If there's a token, then send it along with the requests you make
const token = await authProvider.getToken()
const headers = {
  Authorization: token ? `Bearer ${token}` : undefined,
}
window.fetch('http://example.com/pets', {headers})

Auth in React

In a React application you manage user authenticated state the same way you
manage any state: useState + useEffect (for making the request). When the
user provides a username and password, you make a request and if the request is
successful, you can then use that token to make additional authenticated
requests. Often, in addition to the token, the server will also respond with the
user's information which you can store in state and use it to display the user's
data.

The easiest way to manage displaying the right content to the user based on
whether they've logged in, is to split your app into two parts: Authenticated,
and Unauthenticated. Then you choose which to render based on whether you have
the user's information.

And when the app loads in the first place, you'll call your auth provider's API
to retrieve a token if the user is already logged in. If they are, then you can
show a loading screen while you request the user's data before rendering
anything else. If they aren't, then you know you can render the login screen
right away.

📜 Learn more about this:
https://kentcdodds.com/blog/authentication-in-react-applications

Exercise

Production deploys:

👨‍💼 Our users are excited about the demo, but they really want to start making
their own reading lists out of those books. Our backend engineers have been
working around the clock to get this authentication stuff working for you.

We're using a service called "Auth Provider" (yes, a very clever name, it's a
made-up thing, but should give you a good idea of how to use any typical
authentication provider which is the point).

Here's what you need to know about "Auth Provider":

  • You import it like this: import * as auth from 'auth-provider'
  • Here are the exports you'll need (they're all async):
    • getToken() - resolves to the token if it exists
    • login({username, password}) - resolves to the user if successful
    • register({username, password}) - resolves to the user if successful
    • logout - logs the user out

To make an authenticated request, you'll need to get the token, and attach an
Authorization header to the request set to: Bearer {token}

As for the UI, when the user registers or logs in, they should be shown the
discover page. They should also have a button to logout which will clear the
user's token from the browser and render the home page again.

Files

  • src/app.js

Extra Credit

1. 💯 Load the user's data on page load

Production deploy

👨‍💼 People are complaining that when they refresh the app shows them the login
screen again. Whoops! Looks like we'll need to check if there's a token in
localStorage and make a request to get the user's info if there is.

Luckily, the backend devs gave us an API we can use to get the user's
information by providing the token:

const token = await auth.getToken()
if (token) {
  // we're logged in! Let's go get the user's data:
  client('me', {token}).then(data => {
    console.log(data.user)
  })
} else {
  // we're not logged in. Show the login screen
}

Add this capability to src/app.js (in a React.useEffect()) so users don't
have to re-enter their username and password if the Auth Provider has the token
already.

You'll also need to add the ability to accept the token option in the client
and set that in the Authorization header (remember, it should be set to:
Bearer ${token})

Files:

  • src/app.js
  • src/utils/api-client.js

2. 💯 Use useAsync

Production deploy

Your co-worker came over 🧝‍♀️ because she noticed the app renders the login screen
for a bit while it's requesting the user's information. She then politely
reminded you that you could get loading state and everything for free by using
her useAsync hook. Doh! Let's update ./src/app.js to use useAsync and
solve this loading state issue.

She mentions you'll need to know that you can set the data directly:

const {data, error, isIdle, isLoading, isSuccess, isError, run, setData} =
  useAsync()

const doSomething = () => somethingAsync().then(data => setData(data))

You'll use this for the login and register.

When in isLoading or isIdle state, you can render the FullPageSpinner from
components/lib. If you end up in isError state then you can render this:

<div
  css={{
    color: colors.danger,
    height: '100vh',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center',
  }}
>
  <p>Uh oh... There's a problem. Try refreshing the app.</p>
  <pre>{error.message}</pre>
</div>

Files:

  • src/app.js

3. 💯 automatically logout on 401

Production deploy

If the user's token expires or the user does something they're not supposed to,
the backend can send a 401 request. If that happens, then we'll want to log the
user out and refresh the page automatically so all data is removed from the
page.

Call auth.logout() to delete the user's token from the Auth Provider and call
window.location.assign(window.location) to reload the page for them.

Files:

  • src/utils/api-client.js

4. 💯 Support posting data

Production deploy

It won't be long before we need to actually start sending data along with our
requests, so let's enhance the client to support that use case as well.

Here's how we should be able to use the client when this is all done:

client('http://example.com/pets', {
  token: 'THE_USER_TOKEN',
  data: {name: 'Fluffy', type: 'cat'},
})

// results in fetch getting called with:
// url: http://example.com/pets
// config:
//  - method: 'POST'
//  - body: '{"name": "Fluffy", "type": "cat"}'
//  - headers:
//    - 'Content-Type': 'application/json'
//    - Authorization: 'Bearer THE_USER_TOKEN'

Files:

  • src/utils/api-client.js

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you've just learned, then
fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Build%20React%20Apps&e=04%3A%20Authentication&em=

Render a React App

Render a React App

📝 Your Notes

Exercise

// 🐨 you'll need to import react and createRoot from react-dom up here
import React from 'react'
import {createRoot} from 'react-dom/client'

// 🐨 you'll also need to import the Logo component from './components/logo'
import {Logo} from 'components/logo'

// 🐨 create an App component here and render the logo, the title ("Bookshelf"), a login button, and a register button.
// 🐨 for fun, you can add event handlers for both buttons to alert that the button was clicked
const App = () => {
  const handleBtnClick = e => {
    const btnName = e.target.textContent
    alert(`${btnName} clicked!`)
  }

  return (
    <>
      <Logo width="80" height="80" />
      <h1>Bookshelf</h1>
      <div>
        <button onClick={handleBtnClick}>Login</button>
      </div>
      <div>
        <button onClick={handleBtnClick}>Register</button>
      </div>
    </>
  )
}

// 🐨 use createRoot to render the <App /> to the root element
// 💰 find the root element with: document.getElementById('root')
const root = createRoot(document.getElementById('root'))
root.render(<App />)
export {root}

1. 💯 Use @reach/dialog

import '@reach/dialog/styles.css'
import React, {useState} from 'react'
import {createRoot} from 'react-dom/client'
import Dialog from '@reach/dialog'
import {Logo} from 'components/logo'

const App = () => {
  const [openModal, setOpenModal] = useState('none')

  const handleModalClose = () => setOpenModal('none')

  return (
    <>
      <Logo width="80" height="80" />
      <h1>Bookshelf</h1>
      <div>
        <button onClick={() => setOpenModal('login')}>Login</button>
      </div>
      <div>
        <button onClick={() => setOpenModal('register')}>Register</button>
      </div>
      <Dialog
        aria-label="Login form"
        isOpen={openModal === 'login'}
        onDismiss={handleModalClose}
      >
        <button onClick={handleModalClose}>close</button>
        <h3>Login</h3>
      </Dialog>
      <Dialog
        aria-label="Registration form"
        isOpen={openModal === 'register'}
        onDismiss={handleModalClose}
      >
        <button onClick={handleModalClose}>close</button>
        <h3>Register</h3>
      </Dialog>
    </>
  )
}

const root = createRoot(document.getElementById('root'))
root.render(<App />)
export {root}

2. 💯 Create a LoginForm component

import '@reach/dialog/styles.css'
import React, {useState} from 'react'
import {createRoot} from 'react-dom/client'
import Dialog from '@reach/dialog'
import {Logo} from 'components/logo'

const LoginForm = ({onSubmit, buttonText}) => {
  const handleSubmit = e => {
    e.preventDefault()
    // * Form event target elements shows all elements it has
    // * You can destructure those elements by their ID (<input id='something' />)
    const {username, password} = e.target.elements

    onSubmit({
      username: username.value,
      password: password.value,
    })
  }
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username</label>
        <input id="username" type="text" />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input id="password" type="password" />
      </div>
      <div>
        <button type="submit">{buttonText}</button>
      </div>
    </form>
  )
}

const App = () => {
  const [openModal, setOpenModal] = useState('none')

  const handleModalClose = () => setOpenModal('none')

  // * FormData object: https://developer.mozilla.org/ko/docs/Web/API/FormData/FormData
  const login = formData => console.log('login', formData)
  const register = formData => console.log('register', formData)

  return (
    <>
      <Logo width="80" height="80" />
      <h1>Bookshelf</h1>
      <div>
        <button onClick={() => setOpenModal('login')}>Login</button>
      </div>
      <div>
        <button onClick={() => setOpenModal('register')}>Register</button>
      </div>
      <Dialog
        aria-label="Login form"
        isOpen={openModal === 'login'}
        onDismiss={handleModalClose}
      >
        <button onClick={handleModalClose}>close</button>
        <h3>Login</h3>
        <LoginForm onSubmit={login} buttonText="Login" />
      </Dialog>
      <Dialog
        aria-label="Registration form"
        isOpen={openModal === 'register'}
        onDismiss={handleModalClose}
      >
        <button onClick={handleModalClose}>close</button>
        <h3>Register</h3>
        <LoginForm onSubmit={register} buttonText="Register" />
      </Dialog>
    </>
  )
}

const root = createRoot(document.getElementById('root'))
root.render(<App />)
export {root}

Background

The first step to any React app is to create a component and render it to the
page. In modern applications with modern tools, this means you'll import React
and ReactDOM and use them to create React elements, and render those to a div.

Exercise

Production deploys:

👨‍💼 I'm excited to get started with you! Let's start out by rendering our awesome
logo and the title of our app. We'll eventually want to allow people to login so
let's also render Login and Register buttons.

Files

  • src/index.js

Extra Credit

1. 💯 Use @reach/dialog

Production deploy

👨‍💼 When the user clicks "Login" or "Register", we should open a modal with a
form for them to provide their username and password.

In this extra credit, get the Dialog component from @reach/dialog and make
it open when the user clicks the Login or Register button. It's a fantastic
component with a great API and fantastic accessibility characteristics.

📜 https://reacttraining.com/reach-ui/dialog

💰 as with everything, there are many ways to do this. For me, I actually render
two individual dialogs and toggle which is open based on a openModal state
which can be set to none, login, or register.

💰 Don't forget to include the styles: import '@reach/dialog/styles.css'

Files:

  • src/index.js

2. 💯 Create a LoginForm component

Production deploy

👨‍💼 The user should be able to login or register by providing a username and
password.

For this one, create a LoginForm component which renders a form accepting a
username and password. When the user submits the form, it should call an
onSubmit prop with the username and password. Here's how it will be used:

function Example() {
  function handleSubmit(formData) {
    console.log('login', formData)
  }
  return <LoginForm onSubmit={handleSubmit} buttonText="Login" />
}

That should render a form where the submit button says "Login" and when the user
clicks it, you'll get a console.log with the form's data.

Files:

  • src/index.js

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you've just learned, then
fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Build%20React%20Apps&e=01%3A%20Render%20a%20React%20App&em=

Make HTTP Requests

Make HTTP Requests

📝 Your Notes

Exercise

/** @jsx jsx */
import {jsx} from '@emotion/core'

import './bootstrap'
import Tooltip from '@reach/tooltip'
import {FaSearch} from 'react-icons/fa'
import {Input, BookListUL, Spinner} from './components/lib'
import {BookRow} from './components/book-row'
import {useState, useEffect} from 'react'
import {client} from 'utils/api-client.exercise'

function DiscoverBooksScreen() {
  const [status, setStatus] = useState('idle')
  const [query, setQuery] = useState('')
  const [queried, setQueried] = useState(false)
  const [data, setData] = useState(null)

  const isLoading = status === 'loading'
  const isSuccess = status === 'success'

  useEffect(() => {
    if (!queried) {
      return
    }

    setStatus('loading')
    client(`books?query=${encodeURIComponent(query)}`).then(responseData => {
      setData(responseData)
      setStatus('success')
    })
  }, [queried, query])

  function handleSearchSubmit(event) {
    event.preventDefault()
    setQuery(event.target.elements.search.value)
    setQueried(true)
  }

  return (
    <div
      css={{maxWidth: 800, margin: 'auto', width: '90vw', padding: '40px 0'}}
    >
      <form onSubmit={handleSearchSubmit}>
        <Input
          placeholder="Search books..."
          id="search"
          css={{width: '100%'}}
        />
        <Tooltip label="Search Books">
          <label htmlFor="search">
            <button
              type="submit"
              css={{
                border: '0',
                position: 'relative',
                marginLeft: '-35px',
                background: 'transparent',
              }}
            >
              {isLoading ? <Spinner /> : <FaSearch aria-label="search" />}
            </button>
          </label>
        </Tooltip>
      </form>

      {isSuccess ? (
        data?.books?.length ? (
          <BookListUL css={{marginTop: 20}}>
            {data.books.map(book => (
              <li key={book.id} aria-label={book.title}>
                <BookRow key={book.id} book={book} />
              </li>
            ))}
          </BookListUL>
        ) : (
          <p>No books found. Try another search.</p>
        )
      ) : null}
    </div>
  )
}

export {DiscoverBooksScreen}

function client(endpoint, customConfig = {}) {
  // 🐨 create the config you'll pass to window.fetch
  //    make the method default to "GET"
  // 💰 if you're confused by this, that's fine. Scroll down to the bottom
  // and I've got some code there you can copy/paste.
  // 🐨 call window.fetch(fullURL, config) then handle the json response
  // 📜 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  // 💰 here's how to get the full URL: `${process.env.REACT_APP_API_URL}/${endpoint}`
  const config = {method: 'GET', ...customConfig}

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(response => response.json())
}

export {client}

1. 💯 handle failed requests

function client(endpoint, customConfig = {}) {
  const config = {method: 'GET', ...customConfig}

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      const data = await response.json()
      if (response.ok) {
        return data
      } else {
        return Promise.reject(data)
      }
    })
}

export {client}

// updated useEffect
useEffect(() => {
  if (!queried) {
    return
  }

  setStatus('loading')
  client(`books?query=${encodeURIComponent(query)}`).then(
    responseData => {
      setData(responseData)
      setStatus('success')
    },
    errorData => {
      setError(errorData)
      setStatus('error')
    },
  )
}, [queried, query])
  • The interesting thing about window.fetch is that it won't reject your promise
    unless the network request itself failed. If talking to the server at all
    failed, then you'll get a rejection, but if that talking to the server was
    successful, then you don't get a rejection even if the request itself was a
    non-200 status code.

  • We want our client to reject the promise if the response is not OK, and that's
    what we're going to handle in our API client. Now, remember, that
    response.json is an async call, so I'm going to turn this successHandler
    function into an async function. That way, I can await this response.json, and
    that will give me my data.

  • Now, I'm going to get data, whether the request was successful or not, and I'm
    going to use that data for the Success response or for the rejected promise.

I'm going to determine whether it should resolve or reject based on response.OK.
If the response is OK, then we can simply return the data, and that leaves our
promise in a resolve state, but if the response was not OK, then we're going to
return a Promise.rejects version of the data. That means that our promise will
reject.

2. 💯 use the useAsync hook

const {data, error, run, isLoading, isSuccess, isError} = useAsync()
const [query, setQuery] = useState('')
const [queried, setQueried] = useState(false)

useEffect(() => {
  if (!queried) return

  run(client(`books?query=${encodeURIComponent(query)}`))
}, [queried, query, run])
  • This run() function is what we call anytime we want to trigger a run of
    something asynchronous that will update our data and our error and our status
    values.

  • Well, client(books?query=${encodeURIComponent(query)}) is this promise
    that we get back from clients, so we'll call run with that client promise.
    Then, we'll add a run to our dependencies here. We don't need to worry about
    it though, because run is memoized using useCallback, so that's never going to
    change on us.

  • Anywhere in our application that needs some asynchronous handling can use this
    useAsync hook and not have to manage all of the status of that promise or the
    data, or error state that results from that promise, which really cleaned up
    our code right here.

Background

Our app wouldn't be very interesting without the ability to request data from a
backend for the user to view and interact with. The way to do this in the web is
using HTTP with the window.fetch API. Here's a quick simple example of that
API in action:

window
  .fetch('http://example.com/movies.json')
  .then(response => {
    return response.json()
  })
  .then(data => {
    console.log(data)
  })

All the HTTP methods are supported as well, for example, here's how you would
POST data:

window
  .fetch('http://example.com/movies', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // if auth is required. Each API may be different, but
      // the Authorization header with a token is common.
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(data), // body data type must match "content-type" header
  })
  .then(response => {
    return response.json()
  })
  .then(data => {
    console.log(data)
  })

If the request fails with an unsuccessful status code (>= 400), then the
response object's ok property will be false. It's common to reject the
promise in this case:

window.fetch(url).then(async response => {
  const data = await response.json()
  if (response.ok) {
    return data
  } else {
    return Promise.reject(data)
  }
})

It's good practice to wrap window.fetch in your own function so you can set
defaults (especially handy for authentication). Additionally, it's common to
have "clients" which build upon this wrapper for operations on different
resources.

Integrating this kind of thing with React involves utilizing React's useEffect
hook for making the request and useState for managing the status of the
request as well as the response data and error information.

You might consider making the network request in the event handler. In general I
recommend to do all your side effects inside the useEffect. This is because in
the event handler you don't have any possibility to prevent race conditions, or
to implement any cancellation mechanism.

📜 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

Exercise

Production deploys:

👨‍💼 Our users are getting restless and want to start looking at some books, so
we're putting our login flow to the side for a moment so we can work on the book
search feature. The backend is ready to go for this and we've already set up an
environment variable which we can use in our code for the API url (you can see
that in .env and .env.development). The URL for the search API is:

const endpoint = `${process.env.REACT_APP_API_URL}/books?query=Voice%20of%20War`

Making a request to this endpoint will return this data:

{
  "books": [
    {
      "title": "Voice of War",
      "author": "Zack Argyle",
      "coverImageUrl": "https://images-na.ssl-images-amazon.com/images/I/41JodZ5Vl%2BL.jpg",
      "id": "B084F96GFZ",
      "pageCount": 372,
      "publisher": "Self Published",
      "synopsis": "..."
    }
  ]
}

We've also already designed the page. All that's left is to wire up our design
with the backend. But we've never made a request to the backend yet so you'll
need to create the API client function that we'll use for making all requests
to our API (like searching books). Once that's ready, you can use it in your
component.

Files

  • src/discover.js
  • src/utils/api-client.js

Extra Credit

1. 💯 handle failed requests

Production deploy

Our backend developers try really hard to give you the data you need, but
sometimes things just fail (💰 especially if you send the word "FAIL" as the
query... go ahead, try it).

Add support for showing the user helpful information in the event of a failure.
Our designer gave us this which you can use for the UI:

For the search icon:

// get FaTimes from react-icons
<FaTimes aria-label="error" css={{color: colors.danger}} />
// display this between the search input and the results
{
  isError ? (
    <div css={{color: colors.danger}}>
      <p>There was an error:</p>
      <pre>{error.message}</pre>
    </div>
  ) : null
}

💰 I wasn't joking. For some reason every time you send the backend the word
"FAIL" it results in a failure. Our backend devs are completely baffled, but it
sure makes it easier for you to test the error state out!

Files:

  • src/utils/api-client.js
  • src/discover.js

2. 💯 use the useAsync hook

Production deploy

After you finished with everything, one of the other UI devs 🧝‍♀️ was reviewing
your PR and asked why you didn't use the useAsync hook she wrote last week.
You respond by palming your face 🤦‍♂️ and go back to the drawing board.

useAsync is slightly different from what you've built. Here's an example:

import {useAsync} from 'utils/hooks'

const {data, error, run, isLoading, isError, isSuccess} = useAsync()

// in an event handler/effect/wherever
run(doSomethingThatReturnsAPromise())

This seems to handle your use case well, so let's swap your custom solution with
your co-worker's useAsync hook.

Files:

  • src/discover.js

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you've just learned, then
fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Build%20React%20Apps&e=03%3A%20Make%20HTTP%20Requests&em=

Creating custom components

Creating custom components

📝 Your Notes

Kent

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script type="text/babel">
    function message({ children }) {
      return <div className="message">{children}</div>
    }

    const helloElement = message({ children: 'Hello World' })
    const goodbyeElement = message({ children: 'Goodbye World' })

    const element = (
      <div className="container">
        {helloElement}
        {goodbyeElement}
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Mine

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script type="text/babel">
    // 🐨 Make a function called `message` which returns the JSX we want to share
    const message = (props) => <div className="message">{props.children}</div>

    // 🐨 use that function in place of the divs below with:
    // 💰 {message({children: 'Hello World'})} {message({children: 'Goodbye World'})}
    const element = (
      <div className="container">
        {message({ children: 'Hello World' })}
        {message({ children: 'Goodbye World' })}
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Extra 1

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script type="text/babel">
    function message({ children }) {
      return <div className="message">{children}</div>
    }

    const helloElement = React.createElement(message, { children: 'Hello World' })
    const goodbyeElement = React.createElement(message, { children: 'Goodbye World' })
    console.log({ helloElement })


    const element = (
      <div className="container">
        {helloElement}
        {goodbyeElement}
        {message({ children: 'it just shows a meaningless div' })}
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>
  • There is something different in the way that React manages these elements when we use the createElement API, versus simply calling the function and getting some React elements back.
  • That has some pretty strong implications for our custom components.
  • image

Extra 2

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script type="text/babel">
    function Message({ children }) {
      return <div className="message">{children}</div>
    }

    const element = (
      // <Message>Goodbye World</Message> === {React.createElement(Message, { children: 'Goodbye World' })}
      <div className="container">
        <Message>Hello World</Message>
        <Message>Goodbye World</Message>
        {React.createElement(Message, { children: 'Goodbye World' })}
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>
  • Basically, the JSX syntax specification says that if you start an angle bracket, and you provide a React element type or this element type as a capital letter, then that means it should be a reference to a variable that is in scope.

Extra 3

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script type="text/babel">
    function Message({ subject, greeting }) {
      return (
        <div className="message">
          {greeting}, {subject}
        </div>
      )
    }

    const PropTypes = {
      string(props, propName, componentName) {
        console.log({ props, propName, componentName })
        const type = typeof props[propName]
        if (type !== 'string') {
          return new Error(`Hey, the component ${componentName} needs to be prop ${propName} to be a type of "string" but was passed a ${type}.`)
        }
      }
    }

    Message.propTypes = {
      subject: PropTypes.string,
      greeting: PropTypes.string
    }

    const element = (
      <div className="container">
        <Message greeting="Hello" subject="World" />
        <Message greeting="Goodbye" subject="World" />
        <Message greeting={1} />
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

image

  • In review, to use PropTypes, you have this component, and then you add a PropTypes property onto that component. That's an object where the properties of that object are the props that you want to validate for that component. The value is a function that acts as a validator, where it returns a new error if there's a problem with that particular prop by its prop name.

  • One thing to keep in mind is this is not free. This can actually be problematic for performance. React actually does not do this in production mode. If I were to say production.min to get the minified production built version of React and ReactDOM, and we save this, we're going to get a refresh, and we're no longer going to see those errors. These are no longer running.

  • There's actually in all modern tools that you're going to be using to build your React apps, there are Babel plugins that will just remove this from the code entirely, so you don't have to worry about prop types being included in production, and running and causing performance problems for your applications.

  • That's something to keep in mind as well is that this is a development time-only feature. You will only see it in development, and it is quite awesome.

Extra 4

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
  <script src="https://unpkg.com/[email protected]/prop-types.js"></script>
  <script type="text/babel">
    function Message({ subject, greeting }) {
      return (
        <div className="message">
          {greeting}, {subject}
        </div>
      )
    }

    Message.propTypes = {
      subject: PropTypes.string.isRequired,
      greeting: PropTypes.string.isRequired,
    }

    const element = (
      <div className="container">
        <Message greeting="Hello" subject="World" />
        <Message greeting="Goodbye" subject="World" />
        <Message subject="World" />
        <Message greeting="Goodbye" subject={5} />
      </div>
    )

    // 💯 This is only the first step to making actual React components. The rest is in the extra credit!
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

image

  • PropTypes are nice because it's a runtime, but you should probably be OK just using TypeScript, the static-type definitions that you have there. If you're not using a static-type language like TypeScript, then I do recommend that you implement PropTypes on most of the components in your React application, because it will save your neck absolutely 100 percent. That's PropTypes.

Background

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.)

Exercise

Production deploys:

Let's say the DOM we want to generate is like this:

<div class="container">
  <div class="message">Hello World</div>
  <div class="message">Goodbye World</div>
</div>

In this case, it would be cool if we could reduce the duplication for creating
the React elements for this:

<div className="message">{children}</div>

So we need to make a function which accepts an object argument with a children
property and returns the React element. Then you can interpolate a call to that
function in your JSX.

<div>{message({children: 'Hello World'})}</div>

This is not how we write custom React components, but this is important for you
to understand them. We'll get to custom components in the extra credit.

📜 Read more

Extra Credit

1. 💯 using a custom component with React.createElement

Production deploy

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
second argument.

2. 💯 using a custom component with JSX

Production deploy

We're so close! Just like using JSX for regular divs 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:

ui = <Capitalized /> // React.createElement(Capitalized)
ui = <property.access /> // React.createElement(property.access)
ui = <Property.Access /> // React.createElement(Property.Access)
ui = <Property['Access'] /> // SyntaxError
ui = <lowercase /> // React.createElement('lowercase')
ui = <kebab-case /> // React.createElement('kebab-case')
ui = <Upper-Kebab-Case /> // React.createElement('Upper-Kebab-Case')
ui = <Upper_Snake_Case /> // React.createElement(Upper_Snake_Case)
ui = <lower_snake_case /> // 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

Let's change the Message component a little bit. Make it look like this now:

function Message({subject, greeting}) {
  return (
    <div className="message">
      {greeting}, {subject}
    </div>
  )
}

So now we'll use it like this:

<Message greeting="Hello" subject="World" />
<Message greeting="Goodbye" subject="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:

function FavoriteNumber({favoriteNumber}) {
  return <div>My favorite number is: {favoriteNumber}</div>
}

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:

<FavoriteNumber favoriteNumber="not a number" />

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:

4. 💯 Use the prop-types package

Production deploy

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. Go ahead and get that added to the
page by adding a script tag for it:

<script src="https://unpkg.com/[email protected]/prop-types.js"></script>

Then use that package instead of writing it yourself. Also, make use of the
isRequired feature!

5. 💯 using React Fragments

Production deploy

One feature of JSX that you'll find useful is called
"React Fragments". It's a special
kind of component from React which allows you to position two elements
side-by-side rather than just nested.

The component is available via <React.Fragment> (or a
short syntax that opens
with <> and closes with </>). Replace the <div className="container"> with
a fragment and inspect the DOM to notice that the elements are both rendered as
direct children of root.

🦉 Feedback

Fill out
the feedback form.

Prop Collections and Getters

Prop Collections and Getters

📝 Your Notes

Exercise

// Prop Collections and Getters
// http://localhost:3000/isolated/exercise/04.js

import * as React from 'react'
import {Switch} from '../switch'

function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  // 🐨 Add a property called `togglerProps`. It should be an object that has
  // `aria-pressed` and `onClick` properties.
  // 💰 {'aria-pressed': on, onClick: toggle}
  const togglerProps = {'aria-pressed': on, onClick: toggle}
  return {
    on,
    toggle,
    togglerProps,
  }
}

function App() {
  const {on, togglerProps} = useToggle()
  return (
    <div>
      <Switch on={on} {...togglerProps} />
      <hr />
      <button aria-label="custom-button" {...togglerProps}>
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • The most common use case is the person who's using useToggle probably wants to wire up some switch or button to the state of our useToggle hook.
  • We're going to return an object of toggler props. These toggler props are going to have all the props that we typically would apply to this kind of component.
  • We're going to say aria-pressed for our accessibility friends, and onClick will be a toggle. When the user clicks on it, we'll set the on to the opposite of whatever it is currently. That is the whole exercise right there. It probably looks a little bit too simple, but the pattern that we're talking about here is very important.
  • It is important that we continue to pass the onState and the toggle function here so that people who are building UIs that don't perfectly mesh up with the toggler props that we're providing can continue to build those UIs accessing that state and the mechanism for updating the state.
  • For our case, we get the toggler props, we spread that across the position of the props of our switch, and we do the same for the props.

1. 💯 prop getters

// Prop Collections and Getters
// http://localhost:3000/isolated/exercise/04.js

import * as React from 'react'
import {Switch} from '../switch'

function callAll(...fns) {
  return (...args) => {
    fns.forEach(fn => {
      fn && fn(...args)
    })
  }
}

function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      // onClick: () => {
      //   onClick && onClick()
      //   toggle()
      // },
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  return {
    on,
    toggle,
    getTogglerProps,
  }
}

function App() {
  const {on, getTogglerProps} = useToggle()
  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
          onClick: () => console.info('onButtonClick'),
          id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • Then getTogglerProps can be responsible for accepting all the props that I want to pass, composing them together in a way that works, and then returning all of those props composed together so I can spread them across that button.
  • First thing I'm going to do is I'm going to de-structure this so that I can grab the onClick if it's provided, and then we'll take the rest of the prompts. Let's do a default object here just in case they don't have any props that they care to apply. We can default that to an empty object, so they don't have to pass anything if they don't want to.
  • We'll only call onClick if it's truthy. That works for both of our use cases. Now, we can click on this. It's working fine. We're getting our log. Down here, instead of togglerProps, we can call getTogglerProps.
  • What I'm going to do is I'm going to make this fancy function called callAll(). This is going to take any number of functions, and then it's going to return a function that takes any number of arguments. I don't care what those arguments are. We'll take those functions. For each of those, we'll take that function.
  • If that function exists, then we'll call that function with all the args. Basically, it's a function that I can call passing any number of functions that will return a function that calls all those functions. Definitely, you play around with this a little bit if it's a little confusing to you, but it's pretty fantastic.
  • What it allows me to do here now is I can say, "Give me a function that will call all of the functions I pass you onClick and toggle." It'll call onClick, and then it'll call toggle. It'll only call onClick if that function actually exists.

Background

One liner: The Prop Collections and Getters Pattern allows your hook to
support common use cases for UI elements people build with your hook.

In typical UI components, you need to take accessibility into account. For a
button functioning as a toggle, it should have the aria-pressed attribute set
to true or false if it's toggled on or off. In addition to remembering that,
people need to remember to also add the onClick handler to call toggle.

Lots of the reusable/flexible components and hooks that we'll create have some
common use-cases and it'd be cool if we could make it easier to use our
components and hooks the right way without requiring people to wire things up
for common use cases.

Real World Projects that use this pattern:

Exercise

Production deploys:

In our simple example, this isn't too much for folks to remember, but in more
complex components, the list of props that need to be applied to elements can be
extensive, so it can be a good idea to take the common use cases for our hook
and/or components and make objects of props that people can simply spread across
the UI they render.

Extra Credit

1. 💯 prop getters

Production deploy

Uh oh! Someone wants to use our togglerProps object, but they need to apply
their own onClick handler! Try doing that by updating the App component to
this:

function App() {
  const {on, togglerProps} = useToggle()
  return (
    <div>
      <Switch on={on} {...togglerProps} />
      <hr />
      <button
        aria-label="custom-button"
        {...togglerProps}
        onClick={() => console.info('onButtonClick')}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

Does that work? Why not? Can you change it to make it work?

What if we change the API slightly so that instead of having an object of props,
we call a function to get the props. Then we can pass that function the props we
want applied and that function will be responsible for composing the props
together.

Let's try that. Update the App component to this:

function App() {
  const {on, getTogglerProps} = useToggle()
  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
          onClick: () => console.info('onButtonClick'),
          id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

See if you can make that API work.

🦉 Feedback

Fill out
the feedback form.

Compound Components

Compound Components

📝 Your Notes

Exercise

// Compound Components
// http://localhost:3000/isolated/exercise/02.js

import * as React from 'react'
import {Switch} from '../switch'

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(on => !on)

  // 🐨 replace this with a call to React.Children.map and map each child in
  // props.children to a clone of that child with the props they need using
  // React.cloneElement.
  // 💰 React.Children.map(props.children, child => {/* return child clone here */})
  // 📜 https://reactjs.org/docs/react-api.html#reactchildren
  // 📜 https://reactjs.org/docs/react-api.html#cloneelement
  // return <Switch on={on} onClick={toggle} />

  return React.Children.map(children, child => {
    const newChild = React.cloneElement(child, {on, toggle})
    console.log({child, newChild})
    return newChild
  })
}

// 🐨 Flesh out each of these components

// Accepts `on` and `children` props and returns `children` if `on` is true
const ToggleOn = ({on, children}) => (on ? children : null)

// Accepts `on` and `children` props and returns `children` if `on` is false
const ToggleOff = ({on, children}) => (on ? null : children)

// Accepts `on` and `toggle` props and returns the <Switch /> with those props.
const ToggleButton = ({on, toggle}) => <Switch on={on} onClick={toggle} />

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <ToggleButton />
      </Toggle>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • What we want to do is we want to allow users to render something when the toggle button is on and to render something else when that toggle button is off. Then we can use that toggle button anywhere in this layout to position that wherever we want.
  • How do we take the state values that the toggle is managing and share it implicitly to the toggle on, toggle off, and toggle button components? I say implicitly because, as the user of these compound components, I don't have any way to know whether the toggle is on or off.
  • First, we'll go ahead and flesh these out a little bit, and then we can work on figuring out how to get the toggle to pass props to these things that they need. For our toggle on component, we're going to accept two props, on and children. If on is true, then we'll return the children. Otherwise, we return null.
  • Our toggle off will be very similar. We'll have on and children. If on is true, then we'll return null. Otherwise, we'll return children. We got to spell it correctly. Otherwise, it doesn't work. For our toggle button, we're going to accept on as our state and toggle as the function that we used to change our state.
  • I'm just going to grab this switch code right here and paste it right here. We're going to say, "On for that on to the onProp here." Toggle will be forwarded on to the onClick. Cool. Now, we have these components ready to go, but we don't have any way to pass the on or toggle props to these because we don't have access to those.
  • Those are managed within the toggle component itself. That's where we get a little fancy with our toggle function. One of the props that our toggle function is receiving is children. We're going to take that children prop, and I'm going to use the React.Children.
  • This has a map function on it, which I can pass children to. Then a function, which will get called with every child. That's going to return to me whatever I map these children to. I'm going to return that. You can think of React.Children.map as basically an array.map, except, it works especially for React.Children because the children prop can be a single element, or it can be an array of elements.
  • We're going to make a brand new copy of this child element using React.cloneElement. We have one child that's at the original one that was created by the app component when they'd called into rendering the toggle. Now, we have a new child that we just cloned from that original child. We're going to return that new child. You'll notice that the new child has the props that we want. We go props.
  • In review, what we did here was, we just trusted in faith that we would get the props that we needed for each one of these components. Some of those props are provided to us by the user of our component. Others of these props are provided implicitly. Thanks to the toggle component we're going to be rendered inside of.
  • This toggle component hands those props to us by taking all of its children and mapping those to new children, which are cloned copies of that original child with these additional props passed implicitly. At least, it's implicit from the perspective of the users, and it's explicit from the perspective of this toggle component. That's how you share implicit state for compound components.

1. 💯 Support DOM component children

// Compound Components
// http://localhost:3000/isolated/exercise/02.js

import * as React from 'react'
import {Switch} from '../switch'

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(on => !on)

  return React.Children.map(children, child => {
    if (typeof child.type === 'string') {
      return child
    }

    const newChild = React.cloneElement(child, {on, toggle})
    console.log(newChild.type)
    return newChild
  })

  /* or do this
  return React.Children.map(children, child => {
    if (allowedTypes.includes(child.type)) {
      const newChild = React.cloneElement(child, {on, toggle})
      return newChild
    }
    return child
  })
   */
}

const ToggleOn = ({on, children}) => (on ? children : null)

const ToggleOff = ({on, children}) => (on ? null : children)

const ToggleButton = ({on, toggle}) => <Switch on={on} onClick={toggle} />

function MyToggleButton({on, toggle}) {
  return on ? 'the buton is on yo.' : 'the button is off sooo...'
}

const allowedTypes = [ToggleOn, ToggleOff, ToggleButton]

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <span>Hello</span>
        <ToggleButton />
        <MyToggleButton />
      </Toggle>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • image

Background

One liner: The Compound Components Pattern enables you to provide a set of
components that implicitly share state for a simple yet powerful declarative API
for reusable components.

Compound components are components that work together to form a complete UI. The
classic example of this is <select> and <option> in HTML:

<select>
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
</select>

The <select> is the element responsible for managing the state of the UI, and
the <option> elements are essentially more configuration for how the select
should operate (specifically, which options are available and their values).

Let's imagine that we were going to implement this native control manually. A
naive implementation would look something like this:

<CustomSelect
  options={[
    {value: '1', display: 'Option 1'},
    {value: '2', display: 'Option 2'},
  ]}
/>

This works fine, but it's less extensible/flexible than a compound components
API. For example. What if I want to supply additional attributes on the
<option> that's rendered, or I want the display to change based on whether
it's selected? We can easily add API surface area to support these use cases,
but that's just more for us to code and more for users to learn. That's where
compound components come in really handy!

Real World Projects that use this pattern:

Exercise

Production deploys:

Every reusable component starts out as a simple implementation for a specific
use case. It's advisable to not overcomplicate your components and try to solve
every conceivable problem that you don't yet have (and likely will never have).
But as changes come (and they almost always do), then you'll want the
implementation of your component to be flexible and changeable. Learning how to
do that is the point of much of this workshop.

This is why we're starting with a super simple <Toggle /> component.

In this exercise we're going to make <Toggle /> the parent of a few compound
components:

  • <ToggleOn /> renders children when the on state is true
  • <ToggleOff /> renders children when the on state is false
  • <ToggleButton /> renders the <Switch /> with the on prop set to the on
    state and the onClick prop set to toggle.

We have a Toggle component that manages the state, and we want to render
different parts of the UI however we want. We want control over the presentation
of the UI.

🦉 The fundamental challenge you face with an API like this is the state shared
between the components is implicit, meaning that the developer using your
component cannot actually see or interact with the state (on) or the
mechanisms for updating that state (toggle) that are being shared between the
components.

So in this exercise, we'll solve that problem by providing the compound
components with the props they need implicitly using React.cloneElement.

Here's a simple example of using React.Children.map and React.cloneElement:

function Foo({children}) {
  return React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      id: `i-am-child-${index}`,
    })
  })
}

function Bar() {
  return (
    <Foo>
      <div>I will have id "i-am-child-0"</div>
      <div>I will have id "i-am-child-1"</div>
      <div>I will have id "i-am-child-2"</div>
    </Foo>
  )
}

Extra Credit

1. 💯 Support DOM component children

Production deploy

A DOM component is a built-in component like <div />, <span />, or
<blink />. A composite component is a custom component like <Toggle /> or
<App />.

Try updating the App to this:

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        <span>Hello</span>
        <ToggleButton />
      </Toggle>
    </div>
  )
}

Notice the error message in the console and try to fix it.

🦉 Feedback

Fill out
the feedback form.

useRef and useEffect: DOM interaction

useRef and useEffect: DOM interaction

📝 Your Notes

Exercise

import * as React from 'react'
// eslint-disable-next-line no-unused-vars
import VanillaTilt from 'vanilla-tilt'

function Tilt({children}) {
  // 🐨 create a ref here with React.useRef()
  const tiltRef = React.useRef()

  // 🐨 add a `React.useEffect` callback here and use VanillaTilt to make your
  // div look fancy.
  React.useEffect(() => {
    const tiltNode = tiltRef.current
    VanillaTilt.init(tiltNode, {
      max: 25,
      speed: 400,
      glare: true,
      'max-glare': 0.5,
    })

    return function cleanup() {
      tiltNode.vanillaTilt.destroy()
    }
  }, [])
  // 💰 like this:
  // const tiltNode = tiltRef.current
  // VanillaTilt.init(tiltNode, {
  //   max: 25,
  //   speed: 400,
  //   glare: true,
  //   'max-glare': 0.5,
  // })
  //
  // 💰 Don't forget to return a cleanup function. VanillaTilt.init will add an
  // object to your DOM node to cleanup:
  // `return () => tiltNode.vanillaTilt.destroy()`
  //
  // 💰 Don't forget to specify your effect's dependencies array! In our case
  // we know that the tilt node will never change, so make it `[]`. Ask me about
  // this for a more in depth explanation.

  // 🐨 add the `ref` prop to the `tilt-root` div here:
  return (
    <div className="tilt-root" ref={tiltRef}>
      <div className="tilt-child">{children}</div>
    </div>
  )
}

function App() {
  return (
    <Tilt>
      <div className="totally-centered">vanilla-tilt.js</div>
    </Tilt>
  )
}

export default App
  • useRef.current?

    • What we need to do is we need to create a ref so that we can get a reference to this DOM node. I'm going to create my tiltRef with React.useRef. With that, now I can take that tiltRef. I'm going to come down here, and we'll add a ref to this div for the tiltRef. Awesome.
    • If we go ahead and console.log the tiltRef right here and pop open our DevTools, we've got an object and the current value is that div tilt-root. That's interesting. Let's go ahead and say, ".current," and we refresh. Now, we're getting undefined. What's going on here?
    • What's happening here is the DevTools are trying to be helpful in showing you the current properties of this ref. Right here, the current property is div.tilt-root.
    • What's going on is React goes ahead and calls this function, and our tiltRef gets initialized undefined, because we're not passing an initial value. We could pass a null here; we could pass five. It doesn't really matter. Our initial value here is undefined, so tiltRef.current at this point is going to be undefined.
    • Then we forward that on to this div. Remember, we're not creating DOM nodes here. We're just creating UI descriptor objects. These objects that React uses, these React elements. Then ultimately, those get rendered as DOM nodes to the page.
    • When React renders that DOM node to the page, then it's going to say, "You wanted a reference to this DOM node." I'm going to go ahead and set the current property on this object to that DOM node. Then by the time we expand this, that has already happened, which is why we're able to see what that current property is.
    • The cool thing is that React is able to do this in time for our side effects to run, which is what we care about. We don't really care about accessing the DOM node at this point. We care about accessing the DOM node inside of a side effect handler. React.useEffect. This is where we're going to interact with our DOM nodes.
  • Clean up function for optimization

    • Let's assume for a moment that this tilt could get removed from the page for some reason. If that happens, then vanilla-tilt is still wired up to this tiltNode. That DOM node is going to hang out because it can't be garbage collected. Even though it's not on the page anymore, vanilla-tilt still has event handlers hanging out on it, it has references to that DOM node. What could happen there is we end up with a memory leak in the browser, where we render this and then it goes away from the page, and we render it again, it goes away from the page.
    • Eventually, we're going to have a bunch of DOM nodes just hanging out in memory, wasting our user's memory, resulting in sub-optimal performance. We're going to add this optimization where we return a function called cleanup. That doesn't accept any arguments.
  • Adding empty array for dependency

    • On top of that, as this tiltNode gets rerendered, we're going to have a problem, because this useEffect is called on every single rerender. What that's going to do is it's going to destroy our vanilla-tilt, and then it will reinitialize it every single time this component rerenders. We don't want it to do that. We're going to add this other optimization to have an empty array list of dependencies.
    • We're basically saying, "Hey, I don't need to synchronize the state of the world with the state of the app, because the state of the world that I'm dealing with doesn't actually depend on the state of the app. It just needs to happen once when we're mounted, and then get cleaned up when were unmounted." We cover that with this useEffect.

Background

Often when working with React you'll need to integrate with UI libraries. Some
of these need to work directly with the DOM. Remember that when you do:
<div>hi</div> that's actually syntactic sugar for a React.createElement so
you don't actually have access to DOM nodes in your function component. In fact,
DOM nodes aren't created at all until the ReactDOM.render method is called.
Your function component is really just responsible for creating and returning
React Elements and has nothing to do with the DOM in particular.

So to get access to the DOM, you need to ask React to give you access to a
particular DOM node when it renders your component. The way this happens is
through a special prop called ref.

Here's a simple example of using the ref prop:

function MyDiv() {
  const myDivRef = React.useRef()
  React.useEffect(() => {
    const myDiv = myDivRef.current
    // myDiv is the div DOM node!
    console.log(myDiv)
  }, [])
  return <div ref={myDivRef}>hi</div>
}

After the component has been rendered, it's considered "mounted." That's when
the React.useEffect callback is called and so by that point, the ref should have
its current property set to the DOM node. So often you'll do direct DOM
interactions/manipulations in the useEffect callback.

Exercise

Production deploys:

In this exercise we're going to make a <Tilt /> component that renders a div
and uses the vanilla-tilt library to make it super fancy.

The thing is, vanilla-tilt works directly with DOM nodes to setup event
handlers and stuff, so we need access to the DOM node. But because we're not the
one calling document.createElement (React does) we need React to give it to
us.

So in this exercise we're going to use a ref so React can give us the DOM node
and then we can pass that on to vanilla-tilt.

Additionally, we'll need to clean up after ourselves if this component is
unmounted. Otherwise we'll have event handlers dangling around on DOM nodes that
are no longer in the document.

Alternate:

If you'd prefer to practice refactoring a class that does this to a hook, then
you can open src/exercise/05-classes.js and open that on
an isolated page to
practice that.

🦉 Feedback

Fill out
the feedback form.

useState: tic tac toe

useState: tic tac toe

📝 Your Notes

Exercise

import * as React from 'react'

function Board() {
  // 🐨 squares is the state for this component. Add useState for squares
  const [squares, setSquares] = React.useState(Array(9).fill(null))

  // 🐨 We'll need the following bits of derived state:
  // - nextValue ('X' or 'O')
  // - winner ('X', 'O', or null)
  // - status (`Winner: ${winner}`, `Scratch: Cat's game`, or `Next player: ${nextValue}`)
  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(winner, squares, nextValue)
  // 💰 I've written the calculations for you! So you can use my utilities
  // below to create these variables

  // This is the function your square click handler will call. `square` should
  // be an index. So if they click the center square, this will be `4`.
  function selectSquare(square) {
    // 🐨 first, if there's already winner or there's already a value at the
    // given square index (like someone clicked a square that's already been
    // clicked), then return early so we don't make any state changes
    if (winner || squares[square]) return

    // 🦉 It's typically a bad idea to mutate or directly change state in React.
    // Doing so can lead to subtle bugs that can easily slip into production.
    //
    // 🐨 make a copy of the squares array
    // 💰 `[...squares]` will do it!)
    const squaresCopy = [...squares]

    // 🐨 set the value of the square that was selected
    // 💰 `squaresCopy[square] = nextValue`
    squaresCopy[square] = nextValue

    // 🐨 set the squares to your copy
    setSquares(squaresCopy)
  }

  function restart() {
    // 🐨 reset the squares
    // 💰 `Array(9).fill(null)` will do it!
    setSquares(Array(9).fill(null))
  }

  function renderSquare(i) {
    return (
      <button className="square" onClick={() => selectSquare(i)}>
        {squares[i]}
      </button>
    )
  }

  return (
    <div>
      {/* 🐨 put the status in the div below */}
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
      <button className="restart" onClick={restart}>
        restart
      </button>
    </div>
  )
}

function Game() {
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
    </div>
  )
}

// eslint-disable-next-line no-unused-vars
function calculateStatus(winner, squares, nextValue) {
  return winner
    ? `Winner: ${winner}`
    : squares.every(Boolean)
    ? `Scratch: Cat's game`
    : `Next player: ${nextValue}`
}

// eslint-disable-next-line no-unused-vars
function calculateNextValue(squares) {
  return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
}

// eslint-disable-next-line no-unused-vars
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ]
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

function App() {
  return <Game />
}

export default App

1. 💯 preserve state in localStorage

import * as React from 'react'

function Board() {
  const [squares, setSquares] = React.useState(
    () =>
      JSON.parse(window.localStorage.getItem('squares')) ?? Array(9).fill(null),
  )

  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(winner, squares, nextValue)

  React.useEffect(() => {
    window.localStorage.setItem('squares', JSON.stringify(squares))
  }, [squares])

  function selectSquare(square) {
    if (winner || squares[square]) return

    const squaresCopy = [...squares]
    squaresCopy[square] = nextValue
    setSquares(squaresCopy)
  }

  function restart() {
    setSquares(Array(9).fill(null))
  }

  function renderSquare(i) {
    return (
      <button className="square" onClick={() => selectSquare(i)}>
        {squares[i]}
      </button>
    )
  }

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
      <button className="restart" onClick={restart}>
        restart
      </button>
    </div>
  )
}

function Game() {
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
    </div>
  )
}

// eslint-disable-next-line no-unused-vars
function calculateStatus(winner, squares, nextValue) {
  return winner
    ? `Winner: ${winner}`
    : squares.every(Boolean)
    ? `Scratch: Cat's game`
    : `Next player: ${nextValue}`
}

// eslint-disable-next-line no-unused-vars
function calculateNextValue(squares) {
  return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
}

// eslint-disable-next-line no-unused-vars
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ]
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

function App() {
  return <Game />
}

export default App

Background

A name is one thing, but a real UI is a bit different. Often you need more
than one element of state in your component, so you'll call React.useState
more than once. Please note that each call to React.useState in a given
component will give you a unique state and updater function.

Exercise

Production deploys:

We're going to build tic-tac-toe (with localStorage support)! If you've gone
through React's official tutorial, this was lifted from that (except that
example still uses classes).

You're going to need some managed state and some derived state:

  • Managed State: State that you need to explicitly manage
  • Derived State: State that you can calculate based on other state

squares is the managed state and it's the state of the board in a
single-dimensional array:

[
  'X', 'O', 'X',
  'X', 'O', 'O',
  'X', 'X', 'O'
]

This will start out as an empty array because it's the start of the game.

nextValue will be either the string X or O and is derived state which you
can determine based on the value of squares. We can determine whose turn it is
based on how many "X" and "O" squares there are. We've written this out for you
in a calculateNextValue function at the bottom of the file.

winner will be either the string X or O and is derived state which can
also be determined based on the value of squares and we've provided a
calculateWinner function you can use to get that value.

📜 Read more about derived state in
Don't Sync State. Derive It!

Alternate:

If you'd prefer to practice refactoring a class that does this to a hook, then
you can open src/exercise/04-classes.js and open that on
an isolated page to
practice that.

Extra Credit

1. 💯 preserve state in localStorage

Production deploy

👨‍💼 Our customers want to be able to pause a game, close the tab, and then resume
the game later. Can you store the game's state in localStorage?

2. 💯 useLocalStorageState

Production deploy

It's cool that we can get localStorage support with a simple useEffect, but
it'd be even cooler to use the useLocalStorageState hook that's already
written for us in src/utils.js!

Refactor your code to use that custom hook instead. (This should be a pretty
quick extra credit).

3. 💯 add game history feature

Production deploy

Open http://localhost:3000/isolated/final/04.extra-3.js and see that the extra
version supports keeping a history of the game and allows you to go backward and
forward in time. See if you can implement that!

NOTE: This extra credit is one of the harder extra credits. Don't worry if you
struggle on it!

💰 Tip, in the final example, we store the history of squares in an array of
arrays. [[/* step 0 squares */], [/* step 1 squares */], ...etc], so we have
two states: history and currentStep.

💰 Tip, in the final example, we move the state management from the Board
component to the Game component and that helps a bit. Here's what the JSX
returned from the Game component is in the final version:

return (
  <div className="game">
    <div className="game-board">
      <Board onClick={selectSquare} squares={currentSquares} />
      <button className="restart" onClick={restart}>
        restart
      </button>
    </div>
    <div className="game-info">
      <div>{status}</div>
      <ol>{moves}</ol>
    </div>
  </div>
)

🦉 Feedback

Fill out
the feedback form.

useState: greeting

useState: greeting

📝 Your Notes

Kent

import * as React from 'react'

function Greeting() {
  // 💣 delete this variable declaration and replace it with a React.useState call

  // const array = React.useState('')
  // const name = array[0]
  // const setName = array[1]
  const [name, setName] = React.useState('')

  function handleChange(event) {
    // 🐨 update the name here based on event.target.value
    // const name = ''
    const name = event.target.value
    setName(name)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting />
}

export default App
  • I have no mechanism for saying, "Hey, React, I changed the name value. I want you to re-render this and get some new JSX based on the state update that I have made." That's what React useState Hook is intended to do. It says, "Hey, React; this component can re-render any time this state that I want you to manage is going to change."
  • React gives us the state value for the current render, and then it gives you a mechanism for updating that state, which is the state update or function. When you call that function, then that will trigger the re-render.

1. 💯 accept an initialName

import * as React from 'react'

// * One thing to keep in mind here is if we don't pass that prop,
// * now the initial name is going to be undefined and you're going to see a warning.
// * The warning is that the component is changing an uncontrolled input of type undefined to be controlled.
// * Input elements should not switch between these two. That can cause bugs and unexpected things.
// * We need to make sure that this inputs value is not undefined at any time.
// * What we're going to do is default that to an empty string.
// * If the initial name is not provided, then we'll have the default empty string
// * as the initial state for our name that we're going to pass to our input.
function Greeting({initialName = ''}) {
  const [name, setName] = React.useState(initialName)

  function handleChange(event) {
    const name = event.target.value
    setName(name)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input onChange={handleChange} id="name" value={name} />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting initialName="Alex" />
}

export default App

Background

Normally an interactive application will need to hold state somewhere. In React,
you use special functions called "hooks" to do this. Common built-in hooks
include:

  • React.useState
  • React.useEffect
  • React.useContext
  • React.useRef
  • React.useReducer

Each of these is a special function that you can call inside your custom React
component function to store data (like state) or perform actions (or
side-effects). There are a few more built-in hooks that have special use cases,
but the ones above are what you'll be using most of the time.

Each of the hooks has a unique API. Some return a value (like React.useRef and
React.useContext), others return a pair of values (like React.useState and
React.useReducer), and others return nothing at all (like React.useEffect).

Here's an example of a component that uses the useState hook and an onClick
event handler to update that state:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(count + 1)
  return <button onClick={increment}>{count}</button>
}

React.useState is a function that accepts a single argument. That argument is
the initial state for the instance of the component. In our case, the state will
start as 0.

React.useState returns a pair of values. It does this by returning an array
with two elements (and we use destructuring syntax to assign each of those
values to distinct variables). The first of the pair is the state value and the
second is a function we can call to update the state. We can name these
variables whatever we want. Common convention is to choose a name for the state
variable, then prefix set in front of that for the updater function.

State can be defined as: data that changes over time. So how does this work over
time? When the button is clicked, our increment function will be called at
which time we update the count by calling setCount.

When we call setCount, that tells React to re-render our component. When it
does this, the entire Counter function is re-run, so when React.useState is
called this time, the value we get back is the value that we called setCount
with. And it continues like that until Counter is unmounted (removed from the
application), or the user closes the application.

Exercise

Production deploys:

In this exercise we have a form where you can type in your name and it will give
you a greeting as you type. Fill out the Greeting component so that it manages
the state of the name and shows the greeting as the name is changed.

Extra Credit

1. 💯 accept an initialName

Production deploy

Make the Greeting accept a prop called initialName and initialize the name
state to that value.

🦉 Feedback

Fill out
the feedback form.

Lifting state

Lifting state

📝 Your Notes

Exercise

import * as React from 'react'

function Name({name, onNameChange}) {
  return (
    <div>
      <label htmlFor="name">Name: </label>
      <input id="name" value={name} onChange={onNameChange} />
    </div>
  )
}

// 🐨 accept `animal` and `onAnimalChange` props to this component
function FavoriteAnimal({animal, onAnimalChange}) {
  // 💣 delete this, it's now managed by the App
  // const [animal, setAnimal] = React.useState('')

  return (
    <div>
      <label htmlFor="animal">Favorite Animal: </label>
      <input id="animal" value={animal} onChange={onAnimalChange} />
    </div>
  )
}

// 🐨 uncomment this
function Display({name, animal}) {
  return <div>{`Hey ${name}, your favorite animal is: ${animal}!`}</div>
}

// 💣 remove this component in favor of the new one
// function Display({name}) {
//   return <div>{`Hey ${name}, you are great!`}</div>
// }

function App() {
  // 🐨 add a useState for the animal
  const [name, setName] = React.useState('')
  const [animal, setAnimal] = React.useState('')

  return (
    <form>
      <Name name={name} onNameChange={event => setName(event.target.value)} />
      {/* 🐨 pass the animal and onAnimalChange prop here (similar to the Name component above) */}
      <FavoriteAnimal
        animal={animal}
        onAnimalChange={e => setAnimal(e.target.value)}
      />
      {/* 🐨 pass the animal prop here */}
      <Display name={name} animal={animal} />
    </form>
  )
}

export default App
  • In review, what we did here was we had a situation where there was state being managed in this favoriteAnimal component, and we had a sibling component. These two are rendered side by side. A sibling component that needed access to the state that was being managed inside of this component.

  • What we did was we lifted state. You take that state that we have, and we find the component that is the sibling component that needs to share that state. We find the least common parent between these two components.

  • In our case, that's the app component right here because they're direct siblings, but it could be a couple of parents app depending on where you need to have this state shared. We move the state to that least common parent and then pass on the state and mechanisms for updating the state as props to the respective components.

  • This is called lifting state because you're taking state that's lower in the tree and lifting it to the least common parent higher up in the tree.

1. 💯 colocating state

import * as React from 'react'

function Name() {
  const [name, setName] = React.useState('')

  return (
    <div>
      <label htmlFor="name">Name: </label>
      <input id="name" value={name} onChange={e => setName(e.target.value)} />
    </div>
  )
}

function FavoriteAnimal({animal, onAnimalChange}) {
  return (
    <div>
      <label htmlFor="animal">Favorite Animal: </label>
      <input id="animal" value={animal} onChange={onAnimalChange} />
    </div>
  )
}

function Display({animal}) {
  return <div>{`Your favorite animal is: ${animal}!`}</div>
}

function App() {
  const [animal, setAnimal] = React.useState('')

  return (
    <form>
      <Name />
      <FavoriteAnimal
        animal={animal}
        onAnimalChange={e => setAnimal(e.target.value)}
      />
      <Display animal={animal} />
    </form>
  )
}

export default App
  • In review, all that we did here was we had a change in our display where we no longer needed the name prop. We updated our display component, and then we updated the props that we're passing to the display component, so we're not passing more props than we need.

  • With that, we noticed that the only component that cared about that state was this child component here. We moved that state to the child component, making the child component's API much simpler. We're no longer accepting props, and it can encapsulate the management of its own state by itself.

image

Background

A common question from React beginners is how to share state between two sibling
components. The answer is to
"lift the state" which
basically amounts to finding the lowest common parent shared between the two
components and placing the state management there, and then passing the state
and a mechanism for updating that state down into the components that need it.

Exercise

Production deploys:

👨‍💼 Peter told us we've got a new feature request for the Display component. He
wants us to display the animal the user selects. But that state is managed in
a "sibling" component, so we have to move that management to the least common
parent (App) and then pass it down.

Extra Credit

1. 💯 colocating state

Production deploy

As a community we’re pretty good at lifting state. It becomes natural over time.
One thing that we typically have trouble remembering to do is to push state back
down (or
colocate state).

👨‍💼 Peter told us that now users only want the animal displayed instead of the
name:

function Display({animal}) {
  return <div>{`Your favorite animal is: ${animal}!`}</div>
}

You'll notice that just updating the Display component to this works fine, but
for the extra credit, go through the process of moving state to the components
that need it. You know what you just did for the Animal component? You need to
do the opposite thing for the Name component.

🦉 Feedback

Fill out
the feedback form.

Intro to raw React APIs

Intro to raw React APIs

📝 Your Notes

Kent's solution

<body>
  <div id="root"></div>

  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

  <script type="module">
    const rootElement = document.getElementById('root')

    const element = React.createElement('div', {
      className: 'container',
      children: 'Hello World',
    })

    // React 18
    const root = ReactDOM.createRoot(rootElement)
    root.render(element)
  </script>
</body>

Mine

<!-- Intro to raw React APIs -->
<!-- http://localhost:3000/isolated/exercise/02.html -->

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

  <script type="module">
    const rootElement = document.getElementById('root')

    // 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
    // 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
    const element = React.createElement('div', { className: 'container' }, 'Hello World');

    // This is similar to ReactDOM.createRoot().render()
    // rootElement.append(element)
    ReactDOM.render(element, rootElement);

    // 🐨 Please re-implement the regular document.createElement code above
    // with these React API calls
    // 💰 The example in the markdown file should be a good hint for you.

  </script>
</body>

Extra

<body>
  <div id="root"></div>

  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

  <script type="module">
    const rootElement = document.getElementById('root')
    const helloElement = React.createElement('span', {}, 'Hello')
    const worldElement = React.createElement('span', {}, 'World')
    const element = React.createElement('div',
      { className: 'conttainer' },
      helloElement,
      ' ',
      worldElement
    )

    // or do this
    // const element = React.createElement(
    //   "div",
    //   { className: "container" },
    //   React.createElement("span", null, "Hello"),
    //   React.createElement("span", null, "World")
    // )

    // React 18
    const root = ReactDOM.createRoot(rootElement)
    root.render(element)
  </script>
</body>

Background

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
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

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.

With that in mind, you need two JavaScript files to write React applications for
the web:

  • React: responsible for creating React elements (kinda like
    document.createElement())
  • ReactDOM: responsible for rendering React elements to the DOM (kinda like
    rootElement.append())

Exercise

Production deploys:

Let's convert this to use React! But don't worry, we won't be doing any JSX just
yet... You're going to use raw React APIs here.

In modern applications you'll get React and React DOM files from a "package
registry" like npm (react and
react-dom). But for these first exercises, we'll use
the script files which are available on unpkg.com and
regular script tags so you don't have to bother installing them. So in the
exercise you'll be required to add script tags for these files.

Once you include the script tags, you'll have two new global variables to use:
React and ReactDOM.

Here's a simple example of the API:

const elementProps = {id: 'element-id', children: 'Hello world!'}
const elementType = 'h1'
const reactElement = React.createElement(elementType, elementProps)
const root = ReactDOM.createRoot(rootElement)
root.render(reactElement)

🦉 NOTE: prior to React v18, the API was: ReactDOM.render and that's what
you'll see in the EpicReact.dev videos. This material has been updated, so
you'll want to use the new ReactDOM.createRoot API as demonstrated above.

Alright! Let's do this!

Extra Credit

1. 💯 nesting elements

Production deploy

See if you can figure out how to write the JavaScript + React code to generate
this DOM output:

<body>
  <div id="root">
    <div class="container">
      <span>Hello</span>
      <span>World</span>
    </div>
  </div>
</body>

🦉 Feedback

Fill out
the feedback form.

Style React Components

Style React Components

📝 Your Notes

Exercise: Style a Button with Variants

const buttonVariants = {
  primary: {
    background: '#3f51b5',
    color: 'white',
  },
  secondary: {
    background: '#f1f2f7',
    color: '#434449',
  },
}

const Button = styled.button(
  {
    // default style
    padding: '10px 15px',
    border: '0',
    lineHeight: '1',
    borderRadius: '3px',
  },
  // dynamic style
  ({variant = 'primary'}) => buttonVariants[variant],
)

Exercise: Style Input and Formgroup

// Input
const Input = styled.input({
  borderRadius: '3px',
  border: '1px solid #f1f1f4',
  background: '#f1f2f7',
  padding: '8px 12px',
})

// FormGroup
const FormGroup = styled.div({
  display: 'flex',
  flexDirection: 'column',
})

Exercise: Style with Emotion CSS Prop

/** @jsx jsx */
import {jsx} from '@emotion/core'

    <form
      onSubmit={handleSubmit}
      css={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'stretch',
        '> div': {
          margin: '10px auto',
          width: '100%',
          maxWidth: '300px',
        },
      }}
    >

1. 💯 use the emotion macro

// use this
import styled from '@emotion/styled/macro'

2. 💯 use colors and media queries file

import * as colors from 'styles/colors'
import * as mq from 'styles/media-queries'

const buttonVariants = {
  primary: {
    background: colors.indigo,
    color: colors.base,
  },
  secondary: {
    background: colors.gray,
    color: colors.text,
  },
}

const Dialog = styled(ReachDialog)({
  maxWidth: '450px',
  borderRadius: '3px',
  paddingBottom: '3.5em',
  boxShadow: '0 10px 30px -5px rgba(0, 0, 0, 0.2)',
  margin: '20vh auto',
  [mq.small]: {
    width: '100%',
    margin: '10vh auto',
  },
})

3. 💯 make a loading spinner component

const spin = keyframes({
  '0%': {transform: 'rotate(0deg)'},
  '100%': {transform: 'rotate(360deg)'},
})

const Spinner = styled(FaSpinner)({
  animation: `${spin} 1s linear infinite`,
})

// <Spinner aira-label='loading' />
Spinner.defaultProps = {
  'aira-label': 'loading',
}

Background

There are many ways to style React applications, each approach comes with its
own trade-offs, but ultimately all of them comes back stylesheets and inline
styles.

Because we're using webpack, we can import css files directly into our
application and utilize the cascading nature of CSS to our advantage for some
situations.

After developing production applications at scale, I've found great success
using a library called emotion 👩‍🎤. This library uses an
approach called "CSS-in-JS" which enables you to write CSS in your JavaScript.

There are a lot of benefits to this approach which you can learn about from

There are two ways to use emotion, and typically you use both of them in any
given application for different use cases. The first allows you to make a
component that "carries its styles with it." The second allows you to apply
styles to a component.

Making a styled component with emotion

Here's how you make a "styled component":

import styled from '@emotion/styled'

const Button = styled.button`
  color: turquoise;
`

// <Button>Hello</Button>
//
//     👇
//
// <button className="css-1ueegjh">Hello</button>

This will make a button who's text color is turquoise. It works by creating a
stylesheet at runtime with that class name.

You can also use object syntax (this is my personal preference):

const Button = styled.button({
  color: 'turquoise',
})

You can even accept props by passing a function and returning the styles!

const Box = styled.div(props => {
  return {
    height: props.variant === 'tall' ? 150 : 80,
  }
})

// or with the string form:

const Box = styled.div`
  height: ${props => (props.variant === 'tall' ? '150px' : '80px')};
`

// then you can do:
// <Box >

There's lot more you can do with creating styled components, but that should get
you going for this exercise.

📜 https://emotion.sh/docs/styled

Using emotion's css prop

The styled component is only really useful for when you need to reuse a
component. For one-off styles, it's less useful. You inevitably end up creating
components with meaningless names like "Wrapper" or "Container".

Much more often I find it's nice to write one-off styles as props directly on
the element I'm rendering. Emotion does this using a special prop and a custom
JSX function (similar to React.createElement). You can learn more about how
this works from emotion's docs, but for this exercise, all you need to know is
to make it work, you simply add this to the top of the file where you want to
use the css prop:

/** @jsx jsx */
import {jsx} from '@emotion/core'
import * as React from 'react'

With that, you're ready to use the CSS prop anywhere in that file:

function SomeComponent() {
  return (
    <div
      css={{
        backgroundColor: 'hotpink',
        '&:hover': {
          color: 'lightgreen',
        },
      }}
    >
      This has a hotpink background.
    </div>
  )
}

// or with string syntax:

function SomeOtherComponent() {
  const color = 'darkgreen'

  return (
    <div
      css={css`
        background-color: hotpink;
        &:hover {
          color: ${color};
        }
      `}
    >
      This has a hotpink background.
    </div>
  )
}

Ultimately, this is compiled to something that looks a bit like this:

function SomeComponent() {
  return <div className="css-bp9m3j">This has a hotpink background.</div>
}

With the relevant styles being generated and inserted into a stylesheet to make
this all work.

📜 https://emotion.sh/docs/css-prop

If the /** @jsx jsx */ thing is annoying to you, then you can also install
and configure a
babel preset to set
it up for you automatically. Unfortunately for us, react-scripts doesn't
support customizing the babel configuration.

Note also that for this to work, you need to disable the JSX transform feature
new to React 17. We do this in the .env file with the
DISABLE_NEW_JSX_TRANSFORM=true line.

Exercise

Production deploys:

👨‍💼 Our users are complaining that the app doesn't look very stylish. We need to
give it some pizazz! We need a consistent style for our buttons and we need our
form to look nice.

Your job in this exercise is to create a styled button component that supports
the following API:

<Button variant="primary">Login</Button>
<Button variant="secondary">Register</Button>

Then you can use that for all the buttons.

Once you're finished with that, add support for the css prop and then use it
to make the app we have so far look better.

You may also find a few other cases where making a styled component might be
helpful. Go ahead and make styled components and use the css prop until you're
happy with the way things look.

Don't feel like it must be perfect or exactly the same as the final. Feel
free to express your own design taste here. Remember that the point of this
exercise is for you to learn how to use emotion to style React applications in a
consistent way.

Files

  • src/components/lib.js
  • src/index.js

Extra Credit

1. 💯 use the emotion macro

Production deploy

Take a look at the class names that emotion is generating for you right now.
There are two kinds:

  1. css-1eo10v0-App
  2. css-1350wxy

The difference is the first has the name of the component where the css prop
appears and the second doesn't have any sort of label. A label can help a lot
during debugging. The css prop gets the label for free, but to get the label
applied to styled components, you need to use a special version of the styled
package called a "macro".

- import styled from '@emotion/styled'
+ import styled from '@emotion/styled/macro'

Once you've done that, then all your class names should have a label!

📜 Learn more about macros:

Files:

  • src/components/lib.js

2. 💯 use colors and media queries file

Production deploy

Emotion has a fantastic theming API (📜 https://emotion.sh/docs/theming) which
is great for when users can change the theme of the app on the fly. You can also
use CSS Variables if you like.

In our case, we don't support changing the theme on the fly, but we still want
to keep colors and breakpoints consistent throughout the app. So we've defined
all our colors in styles/colors.js and media-queries in
styles/media-queries.js. So find all the places you're using those values and
replace them with a reference to the values exported from those modules.

💰 Here's a tip:

import * as mq from 'styles/media-queries'

// you can use a media query in an object like so:
// {
//   [mq.small]: {
//     /* small styles */
//   }
// }

// or in a string like so:
// css`
//   ${mq.small} {
//     /* small styles */
//   }
// `

📜 https://emotion.sh/docs/media-queries

Files:

  • src/components/lib.js

3. 💯 make a loading spinner component

Production deploy

Emotion fully supports animations and keyframes. Try to create a spinner
component using this API. For now, you can render it alongside the login button.

You can get a spinner icon via:

import {FaSpinner} from 'react-icons/fa'

// 💰 To make a regular component a "styled component" you can do:
// const Spinner = styled(FaSpinner)({/* styles here */})

📜 https://emotion.sh/docs/keyframes

💰 https://stackoverflow.com/a/14859567/971592

Files:

  • src/components/lib.js
  • src/index.js

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you've just learned, then
fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Build%20React%20Apps&e=02%3A%20Style%20React%20Components&em=

Rendering Arrays

Rendering Arrays

📝 Your Notes

Kent

import * as React from 'react'

const allItems = [
  {id: 'apple', value: '🍎 apple'},
  {id: 'orange', value: '🍊 orange'},
  {id: 'grape', value: '🍇 grape'},
  {id: 'pear', value: '🍐 pear'},
]

function App() {
  const [items, setItems] = React.useState(allItems)

  function addItem() {
    const itemIds = items.map(i => i.id)
    setItems([...items, allItems.find(i => !itemIds.includes(i.id))])
  }

  function removeItem(item) {
    setItems(items.filter(i => i.id !== item.id))
  }

  console.log(
    items.map(item => (
      <li>
        <button onClick={() => removeItem(item)}>remove</button>{' '}
        <label htmlFor={`${item.id}-input`}>{item.value}</label>{' '}
        <input id={`${item.id}-input`} defaultValue={item.value} />
      </li>
    )),
  )

  console.log(
    items.map(item => (
      <li key={item.id}>
        <button onClick={() => removeItem(item)}>remove</button>{' '}
        <label htmlFor={`${item.id}-input`}>{item.value}</label>{' '}
        <input id={`${item.id}-input`} defaultValue={item.value} />
      </li>
    )),
  )

  return (
    <div className="keys">
      <button disabled={items.length >= allItems.length} onClick={addItem}>
        add item
      </button>
      <ul style={{listStyle: 'none', paddingLeft: 0}}>
        {items.map(item => (
          // 🐨 add a key prop to the <li> below. Set it to item.id
          <li key={item.id}>
            <button onClick={() => removeItem(item)}>remove</button>{' '}
            <label htmlFor={`${item.id}-input`}>{item.value}</label>{' '}
            <input id={`${item.id}-input`} defaultValue={item.value} />
          </li>
        ))}
      </ul>
    </div>
  )
}

export default App
  • Result of console.log

    • image
  • Don't use index as key prop

    • Don't use the index. That'll silence the warning but not get rid of the problem. You need to do some unique identifier for the particular item that you're trying to represent when you're rendering an array.
    • It's really important that you pass that key and that the key represents something that is unique to the item so that, as you're moving things around, and making changes, or whatever, React can associate the changes properly.

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:

const ui = (
  <ul>
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
  </ul>
)

But rendering an array of elements is very common:

const list = ['One', 'Two', 'Three']

const ui = (
  <ul>
    {list.map(listItem => (
      <li>{listItem}</li>
    ))}
  </ul>
)

Those will generate the same HTML, but what it actually does is slightly
different. Let's re-write it to see that difference:

const list = ['One', 'Two', 'Three']
const listUI = list.map(listItem => <li>{listItem}</li>)
// notice that listUI is an array
const ui = <ul>{listUI}</ul>

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:

We've got a problem. You may have already noticed the error message in the
console about it. Try this:

  1. Hit the "remove" button on the last list item
  2. Notice that list item is now gone 👍
  3. Hit the "remove" button on the first list item
  4. Notice that everything's mixed up! 😦

Let me describe what's going on here.

Here's the TL;DR: Every React element accepts a special key prop you can use
to help React keep track of elements between updates. If you don't provide it
when rendering a list, React can get things mixed up. The solution is to give
each element a unique (to the array) key prop, and then everything will work
fine.

Let's dive in a little deeper:

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).

To be clear, we know as the developer because we wrote the code, but as far as
React is concerned, we simply gave it some react elements before, we gave it
some after, and now React is trying to compare the before and after with no
knowledge of how the elements got from one position to another.

Sometimes 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 (like the value of an input or focus state), that can be
pretty problematic, which this exercise demonstrates.

To solve this problem, we need to give React a hint so it can associate the old
React elements with the new ones we're giving it due to the change. We do this
using a special prop called the key prop.

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 <input /> for each of the
fruit: the input's value (initialized via the defaultValue prop).

Without a key prop, 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.

So here's the rule:

Whenever you're rendering an array of React elements, each one must have a
unique key prop.

📜 You can learn more about what can go wrong when you don't specify the key
prop in my blog post
Understanding React's key prop.

📜 Also, you can get a deeper understanding in this blog post:
Why React needs a key prop.
That'll give you a bit of what's going on under the hood, so I recommend reading
this!

🐨 The React elements we're rendering are the li elements, so for this
exercise, add a key prop there. You can use the item.id for the value to
ensure that the key value is unique for each element.

🦉 Note, the key only needs to be unique within a given array. So this works
fine:

const element = (
  <ul>
    {list.map(listItem => (
      <li key={listItem.id}>{listItem.value}</li>
    ))}
    {list.map(listItem => (
      <li key={listItem.id}>{listItem.value}</li>
    ))}
  </ul>
)

🦉 In our example, the value of the input is managed by the browser, but this
has even bigger implications when we start working with our own state and
side-effects. It's a little too early to demonstrate this for you, but you
should know that when React removes a component from the DOM, it gets
"unmounted" which will trigger side-effect cleanups, and if new elements are
added then those will be "mounted" and will trigger your side-effects. This can
cause some surprising and problematic issues for your users. So just remember
the rule and always provide a key when rendering an array. Later when you have
more React experience, you can come back to this exercise and expand it a bit
with custom components that manage state and side-effects to observe the
problems caused when you ignore the key.

Extra Credit

1. 💯 Focus Demo

Production deploy

🐨 For this extra credit, open the production deploy above.

You can observe that when we're talking about "state" we're also talking about
keyboard focus as well as what text is selected! As you play around with this,
try selecting text in the inputs and observe how the first two examples differ
from the last one. You'll 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! (Remember,
you'll find the source for this demo in the final directory).

There are some other interesting things you can do with keys as well (like
changing them on an element to intentionally reset the state of a component).
Feel free to play around with that if you like, but we'll be using that in a
future workshop so look forward to it!

🦉 Feedback

Fill out
the feedback form.

Control Props

Control Props

📝 Your Notes

Elaborate on your learnings here in src/exercise/06.md

Background

One liner: The Control Props pattern allows users to completely control
state values within your component. This differs from the state reducer pattern
in the fact that you can not only change the state changes based on actions
dispatched but you also can trigger state changes from outside the component
or hook as well.

Sometimes, people want to be able to manage the internal state of our component
from the outside. The state reducer allows them to manage what state changes are
made when a state change happens, but sometimes people may want to make state
changes themselves. We can allow them to do this with a feature called "Control
Props."

This concept is basically the same as controlled form elements in React that
you've probably used many times: 📜
https://reactjs.org/docs/forms.html#controlled-components

function MyCapitalizedInput() {
  const [capitalizedValue, setCapitalizedValue] = React.useState('')

  return (
    <input
      value={capitalizedValue}
      onChange={e => setCapitalizedValue(e.target.value.toUpperCase())}
    />
  )
}

In this case, the "component" that's implemented the "control props" pattern is
the <input />. Normally it controls state itself (like if you render
<input /> by itself with no value prop). But once you add the value prop,
suddenly the <input /> takes the back seat and instead makes "suggestions" to
you via the onChange prop on the state updates that it would normally make
itself.

This flexibility allows us to change how the state is managed (by capitalizing
the value), and it also allows us to programmatically change the state whenever
we want to, which enables this kind of synchronized input situation:

function MyTwoInputs() {
  const [capitalizedValue, setCapitalizedValue] = React.useState('')
  const [lowerCasedValue, setLowerCasedValue] = React.useState('')

  function handleInputChange(e) {
    setCapitalizedValue(e.target.value.toUpperCase())
    setLowerCasedValue(e.target.value.toLowerCase())
  }

  return (
    <>
      <input value={capitalizedValue} onChange={handleInputChange} />
      <input value={lowerCasedValue} onChange={handleInputChange} />
    </>
  )
}

Real World Projects that use this pattern:

Exercise

Production deploys:

In this exercise, we've created a <Toggle /> component which can accept a prop
called on and another called onChange. These work similar to the value and
onChange props of <input />. Your job is to make those props actually
control the state of on and call the onChange with the suggested changes.

Extra Credit

1. 💯 add read only warning

Production deploy

Take a look at the example in ./src/examples/warnings.js (you can pull it up
at
/isolated/examples/warnings.js).

Notice the warnings when you click the buttons. You should see the following
warnings all related to controlled inputs:

Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
Warning: A component is changing an uncontrolled input of type undefined to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components
Warning: A component is changing a controlled input of type undefined to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components

We should issue the same warnings for people who misuse our controlled props:

  1. Passing on without onChange
  2. Passing a value for on and later passing undefined or null
  3. Passing undefined or null for on and later passing a value

For this first extra credit, create a warning for the read-only situation (the
other extra credits will handle the other cases).

💰 You can use the warning package to do this:

warning(doNotWarn, 'Warning message')

// so:
warning(false, 'This will warn')
warning(true, 'This will not warn')

A real-world component that does this is
@reach/listbox

2. 💯 add a controlled state warning

Production deploy

With that read-only warning in place, next try and add a warning for when the
user changes from controlled to uncontrolled or vice-versa.

3. 💯 extract warnings to a custom hook

Production deploy

Both of those warnings could be useful anywhere so let's go ahead and make a
custom hook for them.

Shout out to the Reach UI team for
the implementation of the useControlledSwitchWarning

4. 💯 don't warn in production

Production deploy

Runtime warnings are helpful during development, but probably not useful in
production. See if you can make this not warn in production.

You can tell whether we're running in production with
process.env.NODE_ENV === 'production'

🦉 Feedback

Fill out
the feedback form.

useContext: simple Counter

useCallback: custom hooks

📝 Your Notes

Exercise

import * as React from 'react'

// 🐨 create your CountContext here with React.createContext
const CountContext = React.createContext()

// 🐨 create a CountProvider component here that does this:
//   🐨 get the count state and setCount updater with React.useState
//   🐨 create a `value` array with count and setCount
//   🐨 return your context provider with the value assigned to that array and forward all the other props
//   💰 more specifically, we need the children prop forwarded to the context provider
function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = [count, setCount]

  return <CountContext.Provider value={value} {...props} />
}

function CountDisplay() {
  // 🐨 get the count from useContext with the CountContext
  const [count] = React.useContext(CountContext)
  return <div>{`The current count is ${count}`}</div>
}

function Counter() {
  // 🐨 get the setCount from useContext with the CountContext
  const [, setCount] = React.useContext(CountContext)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>Increment count</button>
}

function App() {
  return (
    <div>
      {/*
        🐨 wrap these two components in the CountProvider so they can access
        the CountContext value
      */}
      <CountProvider>
        <CountDisplay />
        <Counter />
      </CountProvider>
    </div>
  )
}

export default App
  • In review, what we did here was we created a context. We created a function that uses that context provider to provide a value. Then, we used the useContext hook to consume that value from that CountContext.
  • This is all made possible because our provider is being rendered further up in the tree from the consumers(CountDisplay, Counter), allowing us to have some implicit state shared between this provider and each of these consumers.

1. 💯 create a consumer hook

function useCount() {
  const context = React.useContext(CountContext)

  if (!context) {
    throw new Error(`useCount must be used within CountProvider`)
  }

  return context
}

function CountDisplay() {
  const [count] = useCount()
  return <div>{`The current count is ${count}`}</div>
}

function Counter() {
  const [, setCount] = useCount()
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>Increment count</button>
}
  • Any time we have some code that we want to have elsewhere in our application, we make a function out of it. We'll make a function called UseCount. Instead of counter, we'll say UseCount must be used within the count provider. We can return the context. Now, we'll call UseCount. We can do the same thing for this one, UseCount. Now we don't have to worry about the context value at all. We still get that nice error message that tells us exactly what we're supposed to do.
  • Before, this was React useContext, and we passed this countContext here. Now, we want to validate that the context value exists. Otherwise, we'll throw a useful error message, so people know what they need to do to fix the problem. That simplifies the consumers, and the provider simplifies the providing component as well. This is typically how I'm going to useContext for most situations.

2. 💯 caching in a context provider

import * as React from 'react'
import {
  fetchPokemon,
  PokemonForm,
  PokemonDataView,
  PokemonInfoFallback,
  PokemonErrorBoundary,
} from '../pokemon'
import {useAsync} from '../utils'

// 🐨 Create a PokemonCacheContext
const PokemonCacheContext = React.createContext()

// 🐨 create a PokemonCacheProvider function
// 🐨 useReducer with pokemonCacheReducer in your PokemonCacheProvider
// 💰 you can grab the one that's in PokemonInfo
// 🐨 return your context provider with the value assigned to what you get back from useReducer
// 💰 value={[cache, dispatch]}
// 💰 make sure you forward the props.children!
function PokemonCacheProvider(props) {
  const [cache, dispatch] = React.useReducer(pokemonCacheReducer, {})
  const value = [cache, dispatch]

  return <PokemonCacheContext.Provider value={value} {...props} />
}

function usePokemonCache() {
  const context = React.useContext(PokemonCacheContext)

  if (!context) {
    throw new Error(`usePokemonCache must be used within PokemonCacheProvider`)
  }

  return context
}

function pokemonCacheReducer(state, action) {
  switch (action.type) {
    case 'ADD_POKEMON': {
      return {...state, [action.pokemonName]: action.pokemonData}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function PokemonInfo({pokemonName}) {
  // 💣 remove the useReducer here (or move it up to your PokemonCacheProvider)
  const [cache, dispatch] = usePokemonCache()
  console.log(cache, dispatch)
  // 🐨 get the cache and dispatch from useContext with PokemonCacheContext

  const {data: pokemon, status, error, run, setData} = useAsync()

  React.useEffect(() => {
    if (!pokemonName) {
      return
    } else if (cache[pokemonName]) {
      setData(cache[pokemonName])
    } else {
      run(
        fetchPokemon(pokemonName).then(pokemonData => {
          dispatch({type: 'ADD_POKEMON', pokemonName, pokemonData})
          return pokemonData
        }),
      )
    }
  }, [cache, dispatch, pokemonName, run, setData])

  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    throw error
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }
}

function PreviousPokemon({onSelect}) {
  // 🐨 get the cache from useContext with PokemonCacheContext
  const [cache] = usePokemonCache()
  return (
    <div>
      Previous Pokemon
      <ul style={{listStyle: 'none', paddingLeft: 0}}>
        {Object.keys(cache).map(pokemonName => (
          <li key={pokemonName} style={{margin: '4px auto'}}>
            <button
              style={{width: '100%'}}
              onClick={() => onSelect(pokemonName)}
            >
              {pokemonName}
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

function PokemonSection({onSelect, pokemonName}) {
  // 🐨 wrap this in the PokemonCacheProvider so the PreviousPokemon
  // and PokemonInfo components have access to that context.
  return (
    <PokemonCacheProvider>
      <div style={{display: 'flex'}}>
        <PreviousPokemon onSelect={onSelect} />
        <div className="pokemon-info" style={{marginLeft: 10}}>
          <PokemonErrorBoundary
            onReset={() => onSelect('')}
            resetKeys={[pokemonName]}
          >
            <PokemonInfo pokemonName={pokemonName} />
          </PokemonErrorBoundary>
        </div>
      </div>
    </PokemonCacheProvider>
  )
}

function App() {
  const [pokemonName, setPokemonName] = React.useState(null)

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleSelect(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <PokemonSection onSelect={handleSelect} pokemonName={pokemonName} />
    </div>
  )
}

export default App

Background

Memoization in general

Memoization: a performance optimization technique which eliminates the need to
recompute a value for a given input by storing the original computation and
returning that stored value when the same input is provided. Caching is a form
of memoization. Here's a simple implementation of memoization:

const values = {}
function addOne(num: number) {
  if (values[num] === undefined) {
    values[num] = num + 1 // <-- here's the computation
  }
  return values[num]
}

One other aspect of memoization is value referential equality. For example:

const dog1 = new Dog('sam')
const dog2 = new Dog('sam')
console.log(dog1 === dog2) // false

Even though those two dogs have the same name, they are not the same. However,
we can use memoization to get the same dog:

const dogs = {}
function getDog(name: string) {
  if (dogs[name] === undefined) {
    dogs[name] = new Dog(name)
  }
  return dogs[name]
}

const dog1 = getDog('sam')
const dog2 = getDog('sam')
console.log(dog1 === dog2) // true

You might have noticed that our memoization examples look very similar.
Memoization is something you can implement as a generic abstraction:

function memoize<ArgType, ReturnValue>(cb: (arg: ArgType) => ReturnValue) {
  const cache: Record<ArgType, ReturnValue> = {}
  return function memoized(arg: ArgType) {
    if (cache[arg] === undefined) {
      cache[arg] = cb(arg)
    }
    return cache[arg]
  }
}

const addOne = memoize((num: number) => num + 1)
const getDog = memoize((name: string) => new Dog(name))

Our abstraction only supports one argument, if you want to make it work for any
type/number of arguments, knock yourself out.

Memoization in React

Luckily, in React we don't have to implement a memoization abstraction. They
made two for us! useMemo and useCallback. For more on this read:
Memoization and React.

You know the dependency list of useEffect? Here's a quick refresher:

React.useEffect(() => {
  window.localStorage.setItem('count', count)
}, [count]) // <-- that's the dependency list

Remember that the dependency list is how React knows whether to call your
callback (and if you don't provide one then React will call your callback every
render). It does this to ensure that the side effect you're performing in the
callback doesn't get out of sync with the state of the application.

But what happens if I use a function in my callback?

const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
  updateLocalStorage()
}, []) // <-- what goes in that dependency list?

We could just put the count in the dependency list and that would
actually/accidentally work, but what would happen if one day someone were to
change updateLocalStorage?

- const updateLocalStorage = () => window.localStorage.setItem('count', count)
+ const updateLocalStorage = () => window.localStorage.setItem(key, count)

Would we remember to update the dependency list to include the key? Hopefully
we would. But this can be a pain to keep track of dependencies. Especially if
the function that we're using in our useEffect callback is coming to us from
props (in the case of a custom component) or arguments (in the case of a custom
hook).

Instead, it would be much easier if we could just put the function itself in the
dependency list:

const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
  updateLocalStorage()
}, [updateLocalStorage]) // <-- function as a dependency

The problem with that though it will trigger the useEffect to run every
render. This is because updateLocalStorage is defined inside the component
function body. So it's re-initialized every render. Which means it's brand new
every render. Which means it changes every render. Which means... you guessed
it, our useEffect callback will be called every render!

This is the problem useCallback solves. And here's how you solve it

const updateLocalStorage = React.useCallback(
  () => window.localStorage.setItem('count', count),
  [count], // <-- yup! That's a dependency list!
)
React.useEffect(() => {
  updateLocalStorage()
}, [updateLocalStorage])

What that does is we pass React a function and React gives that same function
back to us... Sounds kinda useless right? Imagine:

// this is not how React actually implements this function. We're just imagining!
function useCallback(callback) {
  return callback
}

Uhhh... But there's a catch! On subsequent renders, if the elements in the
dependency list are unchanged, instead of giving the same function back that we
give to it, React will give us the same function it gave us last time. So
imagine:

// this is not how React actually implements this function. We're just imagining!
let lastCallback
function useCallback(callback, deps) {
  if (depsChanged(deps)) {
    lastCallback = callback
    return callback
  } else {
    return lastCallback
  }
}

So while we still create a new function every render (to pass to useCallback),
React only gives us the new one if the dependency list changes.

In this exercise, we're going to be using useCallback, but useCallback is
just a shortcut to using useMemo for functions:

// the useMemo version:
const updateLocalStorage = React.useMemo(
  // useCallback saves us from this annoying double-arrow function thing:
  () => () => window.localStorage.setItem('count', count),
  [count],
)

// the useCallback version
const updateLocalStorage = React.useCallback(
  () => window.localStorage.setItem('count', count),
  [count],
)

🦉 A common question with this is: "Why don't we just wrap every function in
useCallback?" You can read about this in my blog post
When to useMemo and useCallback.

🦉 And if the concept of a "closure" is new or confusing to you, then
give this a read. (Closures are one of the reasons
it's important to keep dependency lists correct.)

Exercise

Production deploys:

People tend to find this exercise more difficult, so I strongly advise
spending some time understanding how the code works before making any changes!

Also, one thing to keep in mind is that React hooks are a great foundation upon
which to build libraries and many have been built. For that reason, you don't
often need to go this deep into making custom hooks. So if you find this one
isn't clicking for you, know that you are learning and when you do face a
situation when you need to use this knowledge, you'll be able to come back and
it will click right into place.

👨‍💼 Peter the Product Manager told us that we've got more features coming our way
that will require managing async state. We've already got some code for our
pokemon lookup feature (if you've gone through the "React Hooks" workshop
already, then this should be familiar, if not, spend some time playing with the
app to get up to speed with what we're dealing with here). We're going to
refactor out the async logic so we can reuse this in other areas of the app.

So, your job is to extract the logic from the PokemonInfo component into a
custom and generic useAsync hook. In the process you'll find you need to do
some fancy things with dependencies (dependency arrays are the biggest challenge
to deal with when making custom hooks).

NOTE: In this part of the exercise, we don't need useCallback. We'll add it in
the extra credits. It's important that you work on this refactor first so
you can appreciate the value useCallback provides in certain circumstances.

Extra Credit

1. 💯 use useCallback to empower the user to customize memoization

Production deploy

Unfortunately, the ESLint plugin is unable to determine whether the
dependencies argument is a valid argument for useEffect which is a shame,
and normally I'd say just ignore it and move on. But, there's another solution
to this problem which I think is probably better.

Instead of accepting dependencies to useAsync, why don't we just treat the
asyncCallback as a dependency? Any time asyncCallback changes, we know that
we should call it again. The problem is that because our asyncCallback depends
on the pokemonName which comes from props, it has to be defined within the
body of the component, which means that it will be defined on every render which
means it will be new every render. This is where React.useCallback comes in!

Here's another example of the React.useCallback API:

function ConsoleGreeting(props) {
  const greet = React.useCallback(
    greeting => console.log(`${greeting} ${props.name}`),
    [props.name],
  )

  React.useEffect(() => {
    const helloGreeting = 'Hello'
    greet(helloGreeting)
  }, [greet])
  return <div>check the console</div>
}

The first argument to useCallback is the callback you want called, the second
argument is an array of dependencies which is similar to useEffect. When one
of the dependencies changes between renders, the callback you passed in the
first argument will be the one returned from useCallback. If they do not
change, then you'll get the callback which was returned the previous time (so
the callback remains the same between renders).

So we only want our asyncCallback to change when the pokemonName changes.
See if you can make things work like this:

// 🐨 you'll need to wrap asyncCallback in React.useCallback
function asyncCallback() {
  if (!pokemonName) {
    return
  }
  return fetchPokemon(pokemonName)
}

// 🐨 you'll need to update useAsync to remove the dependencies and list the
// async callback as a dependency.
const state = useAsync(asyncCallback)

2. 💯 return a memoized run function from useAsync

Production deploy

Requiring users to provide a memoized value is fine. You can document it as part
of the API and expect people to just read the docs right? lol, that's hilarious
😂 It'd be WAY better if we could redesign the API a bit so we (as the hook
developers) are the ones who have to memoize the function, and the users of our
hook don't have to worry about it.

So see if you can redesign this a little bit by providing a (memoized) run
function that people can call in their own useEffect like this:

// 💰 destructuring this here now because it just felt weird to call this
// "state" still when it's also returning a function called "run" 🙃
const {data: pokemon, status, error, run} = useAsync({ status: pokemonName ? 'pending' : 'idle' })

React.useEffect(() => {
  if (!pokemonName) {
    return
  }
  // 💰 note the absence of `await` here. We're literally passing the promise
  // to `run` so `useAsync` can attach it's own `.then` handler on it to keep
  // track of the state of the promise.
  const pokemonPromise = fetchPokemon(pokemonName)
  run(pokemonPromise)
}, [pokemonName, run])

3. 💯 make safeDispatch with useCallback, useRef, and useEffect

Production deploy

NOTICE: Things have changed slightly. The app you're running the exercises
in was changed since the videos were recorded and you can no longer see this
issue by changing the exercise. All the exercises are now rendered in an iframe
on the exercise pages, so when you go to a different exercise, you're
effectively "closing" the page, so all JS execution for that exercise stops.

So I've added a little checkbox which you can use to mount and unmount the
component with ease. This has the benefit of also working on the isolated page
as well. On the exercise page, you'll want to make sure that your console output
is showing the output from the iframe by
selecting the right context.

I've also added a test for this one to help make sure you've got it right.

Also notice that while what we're doing here is still useful and you'll learn
valuable skills, the warning we're suppressing
goes away in React v18.

Phew, ok, back to your extra credit!

This one's a bit tricky, and I'm going to be intentionally vague here to give
you a bit of a challenge, but consider the scenario where we fetch a pokemon,
and before the request finishes, we change our mind and navigate to a different
page (or uncheck the mount checkbox). In that case, the component would get
removed from the page ("unmounted") and when the request finally does complete,
it will call dispatch, but because the component has been removed from the
page, we'll get this warning from React:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

The best solution to this problem would be to
cancel the request,
but even then, we'd have to handle the error and prevent the dispatch from
being called for the rejected promise.

So see whether you can work out a solution for preventing dispatch from being
called if the component is unmounted. Depending on how you implement this, you
might need useRef, useCallback, and useEffect.

🦉 Other notes

useEffect and useCallback

The use case for useCallback in the exercise is a perfect example of the types
of problems useCallback is intended to solve. However the examples in these
instructions are intentionally contrived. You can simplify things a great deal
by not extracting code from useEffect into functions that you then have to
memoize with useCallback. Read more about this here:
Myths about useEffect.

useCallback use cases

The entire purpose of useCallback is to memoize a callback for use in
dependency lists and props on memoized components (via React.memo, which you
can learn more about from the performance workshop). The only time it's useful
to use useCallback is when the function you're memoizing is used in one of
those two situations.

🦉 Feedback

Fill out
the feedback form.

Hooks Flow

Hooks Flow

import * as React from 'react'

function Child() {
  console.log('%c    Child: render start', 'color: MediumSpringGreen')

  const [count, setCount] = React.useState(() => {
    console.log('%c    Child: useState(() => 0)', 'color: tomato')
    return 0
  })

  React.useEffect(() => {
    console.log('%c    Child: useEffect(() => {})', 'color: LightCoral')
    return () => {
      console.log(
        '%c    Child: useEffect(() => {}) cleanup 🧹',
        'color: LightCoral',
      )
    }
  })

  React.useEffect(() => {
    console.log(
      '%c    Child: useEffect(() => {}, [])',
      'color: MediumTurquoise',
    )
    return () => {
      console.log(
        '%c    Child: useEffect(() => {}, []) cleanup 🧹',
        'color: MediumTurquoise',
      )
    }
  }, [])

  React.useEffect(() => {
    console.log('%c    Child: useEffect(() => {}, [count])', 'color: HotPink')
    return () => {
      console.log(
        '%c    Child: useEffect(() => {}, [count]) cleanup 🧹',
        'color: HotPink',
      )
    }
  }, [count])

  const element = (
    <button onClick={() => setCount(previousCount => previousCount + 1)}>
      {count}
    </button>
  )

  console.log('%c    Child: render end', 'color: MediumSpringGreen')

  return element
}

function App() {
  console.log('%cApp: render start', 'color: MediumSpringGreen')

  const [showChild, setShowChild] = React.useState(() => {
    console.log('%cApp: useState(() => false)', 'color: tomato')
    return false
  })

  React.useEffect(() => {
    console.log('%cApp: useEffect(() => {})', 'color: LightCoral')
    return () => {
      console.log('%cApp: useEffect(() => {}) cleanup 🧹', 'color: LightCoral')
    }
  })

  React.useEffect(() => {
    console.log('%cApp: useEffect(() => {}, [])', 'color: MediumTurquoise')
    return () => {
      console.log(
        '%cApp: useEffect(() => {}, []) cleanup 🧹',
        'color: MediumTurquoise',
      )
    }
  }, [])

  React.useEffect(() => {
    console.log('%cApp: useEffect(() => {}, [showChild])', 'color: HotPink')
    return () => {
      console.log(
        '%cApp: useEffect(() => {}, [showChild]) cleanup 🧹',
        'color: HotPink',
      )
    }
  }, [showChild])

  const element = (
    <>
      <label>
        <input
          type="checkbox"
          checked={showChild}
          onChange={e => setShowChild(e.target.checked)}
        />{' '}
        show child
      </label>
      <div
        style={{
          padding: 10,
          margin: 10,
          height: 50,
          width: 50,
          border: 'solid',
        }}
      >
        {showChild ? <Child /> : null}
      </div>
    </>
  )

  console.log('%cApp: render end', 'color: MediumSpringGreen')

  return element
}

export default App

hook-flow

Mount

  • The way that hooks work is we start out with our lazy initializers; that's the function API for useState where you pass in a function. That's what we call a lazy initializer.
  • When our component is mounting to the page when it's first being rendered, it's never been on the page before, the first thing that React is going to do is it's going to run those lazy initializers.
  • Then, it's going to run the rest of our render function. That's all of the contents of our function, where that useState resides. Then, it's going to do updating the DOM. We say, "Hey, React, I want you to put a div right here." React's going to say, "OK, cool. Let me make that div." That's what "React updates the DOM" is all about.
  • At that point, it's going to run LayoutEffect, which you'll learn about in the future. This is basically like a useEffect callback. It's very similar, just slight differences. We can learn about that later.
  • After it runs the LayoutEffect, React is going to stop running and say, "Hey, Browser. By the way, I updated the DOM. Why don't you go ahead and paint the screen?" What that means is the browser says, "Oh, cool. I've got some DOM updates. Let me take those DOM updates and show the user what those DOM updates were."
  • There's a period of time between when you add a class name to an element and when the user sees that change reflected in what is being shown to them. That process is called painting. That's what happens as soon as React updates the DOM and then runs LayoutEffect. It will allow the browser to paint the screen. Then, it runs your effects, updating localStorage, whatever it is that you're doing.

Update

  • We hang out and wait for the user to do some sort of interaction, or some subscription to update, or whatever. Ultimately, we get some state update. That triggers an update. We go through this process. We don't run those lazy initializers this time. Instead, just run the contents of the component function. That's the render phase.
  • Then React will update the DOM again, saying, "Hey, DOM, we need to update this className or add this div or whatever." Then React will actually run what's called the cleanup phase for our LayoutEffects. We'll look at that in our example here in just a second. Once it runs those cleanups, then it will run the LayoutEffects. It will yield to the browser to say, "Hey, browser. I made some updates to the DOM, why don't you show those to the user."
  • The browser is like, "Cool, thanks, React." Once the browser is done updating the screen, then React will say, "Hey, I'm going to go clean up any side effects that we had in the last render, and then I'm going to run all the new effects that need to be run in this render."

Unmount

  • Then, ultimately, the user navigates away from the page, or we click on a checkbox, and something goes away. Somehow, a component gets removed from the screen. All that happens in that phase, it's called unmounting. All that is going to happen is a cleanup of our layout effects, and then a cleanup of our effects.

useImperativeHandle: scroll to top/bottom

useImperativeHandle: scroll to top/bottom

📝 Your Notes

Exercise

Background

When we had class components, we could do stuff like this:

class MyInput extends React.Component {
  _inputRef = React.createRef()
  focusInput = () => this._inputRef.current.focus()
  render() {
    return <input ref={this._inputRef} />
  }
}

class App extends React.Component {
  _myInputRef = React.createRef()
  handleClick = () => this._myInputRef.current.focusInput()
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Focus on the input</button>
        <MyInput ref={this._myInputRef} />
      </div>
    )
  }
}

The key I want to point out in the example here is that bit above that says:
<MyInput ref={this._myInputRef} />. What this does is give you access to the
component instance.

With function components, there is no component instance, so this won't work:

function MyInput() {
  const inputRef = React.useRef()
  const focusInput = () => inputRef.current.focus()
  // where do I put the focusInput method??
  return <input ref={inputRef} />
}

You'll actually get an error if you try to pass a ref prop to a function
component. So how do we solve this? Well, React has had this feature called
forwardRef for quite a while. So we could do that:

const MyInput = React.forwardRef(function MyInput(props, ref) {
  const inputRef = React.useRef()
  ref.current = {
    focusInput: () => inputRef.current.focus(),
  }
  return <input ref={inputRef} />
})

This actually works, however there are some edge case bugs with this approach
when applied in React's future concurrent mode/suspense feature (also it doesn't
support callback refs). So instead, we'll use the useImperativeHandle hook to
do this:

const MyInput = React.forwardRef(function MyInput(props, ref) {
  const inputRef = React.useRef()
  React.useImperativeHandle(ref, () => {
    return {
      focusInput: () => inputRef.current.focus(),
    }
  })
  return <input ref={inputRef} />
})

This allows us to expose imperative methods to developers who pass a ref prop to
our component which can be useful when you have something that needs to happen
and is hard to deal with declaratively.

NOTE: most of the time you should not need useImperativeHandle. Before you
reach for it, really ask yourself whether there's ANY other way to accomplish
what you're trying to do. Imperative code can sometimes be really hard to
follow and it's much better to make your APIs declarative if possible. For
more on this, read
Imperative vs Declarative Programming

Exercise

Production deploys:

For this exercise, we're going to use the simulated chat from the last exercise,
except we've added scroll to top and scroll to bottom buttons. Your job is to
expose the imperative methods scrollToTop and scrollToBottom on a ref so the
parent component can call those directly.

🦉 Feedback

Fill out
the feedback form.

useCallback: custom hooks

useCallback: custom hooks

📝 Your Notes

Exercise

import * as React from 'react'
import {
  fetchPokemon,
  PokemonForm,
  PokemonDataView,
  PokemonInfoFallback,
  PokemonErrorBoundary,
} from '../pokemon'

// 🐨 this is going to be our generic asyncReducer
function pokemonInfoReducer(state, action) {
  switch (action.type) {
    case 'pending': {
      return {status: 'pending', data: null, error: null}
    }
    case 'resolved': {
      return {status: 'resolved', data: action.data, error: null}
    }
    case 'rejected': {
      return {status: 'rejected', data: null, error: action.error}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function useAsync(asyncCallback, initialState, dependencies) {
  const [state, dispatch] = React.useReducer(pokemonInfoReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  React.useEffect(() => {
    const promise = asyncCallback()
    if (!promise) {
      return
    }

    dispatch({type: 'pending'})

    promise.then(
      data => {
        dispatch({type: 'resolved', data})
      },
      error => {
        dispatch({type: 'rejected', error})
      },
    )
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, dependencies)

  return state
}

function PokemonInfo({pokemonName}) {
  const state = useAsync(
    () => {
      if (!pokemonName) {
        return
      }
      return fetchPokemon(pokemonName)
    },
    {
      status: pokemonName ? 'pending' : 'idle',
      data: null,
      error: null,
    },
    [pokemonName],
  )

  const {data: pokemon, status, error} = state

  switch (status) {
    case 'idle':
      return <span>Submit a pokemon</span>
    case 'pending':
      return <PokemonInfoFallback name={pokemonName} />
    case 'rejected':
      throw error
    case 'resolved':
      return <PokemonDataView pokemon={pokemon} />
    default:
      throw new Error('This should be impossible')
  }
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleReset() {
    setPokemonName('')
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        <PokemonErrorBoundary onReset={handleReset} resetKeys={[pokemonName]}>
          <PokemonInfo pokemonName={pokemonName} />
        </PokemonErrorBoundary>
      </div>
    </div>
  )
}

function AppWithUnmountCheckbox() {
  const [mountApp, setMountApp] = React.useState(true)
  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={mountApp}
          onChange={e => setMountApp(e.target.checked)}
        />{' '}
        Mount Component
      </label>
      <hr />
      {mountApp ? <App /> : null}
    </div>
  )
}

export default AppWithUnmountCheckbox

1. 💯 use useCallback to empower the user to customize memoization

function useAsync(asyncCallback, initialState, dependencies) {
  const [state, dispatch] = React.useReducer(pokemonInfoReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  React.useEffect(() => {
    const promise = asyncCallback()
    if (!promise) {
      return
    }

    dispatch({type: 'pending'})

    promise.then(
      data => {
        dispatch({type: 'resolved', data})
      },
      error => {
        dispatch({type: 'rejected', error})
      },
    )
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, dependencies)

  return state
}

function PokemonInfo({pokemonName}) {
  const state = useAsync(
    () => {
      if (!pokemonName) {
        return
      }
      return fetchPokemon(pokemonName)
    },
    {
      status: pokemonName ? 'pending' : 'idle',
      data: null,
      error: null,
    },
    [pokemonName],
  )

...
  • Here's the problem that we talked about earlier. This function (the function inside useAsync before) is going to be redefined on every render. That means that every render that's used async is going to call this function, even if the Pokemon name did not change. That could absolutely be a problem.
  • The next thing that we're going to do is, we need to figure out a way to make this function only change when the Pokemon name changes. That is exactly what useCallback does. I'm going to make an async callback here with React useCallback.
  • React is going to make sure that this callback that we get assigned to async callback will be the same one that we've created in the first place until the Pokemon name changes. Then React will give us the new one that we created with that new Pokemon name in its closure. We get a stable, consistent async callback here, which we can pass to useAsync.

2. 💯 return a memoized run function from useAsync

function useAsync(initialState) {
  const [state, dispatch] = React.useReducer(pokemonInfoReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  const {data, error, status} = state

  const run = React.useCallback(promise => {
    dispatch({type: 'pending'})
    promise.then(
      data => {
        dispatch({type: 'resolved', data})
      },
      error => {
        dispatch({type: 'rejected', error})
      },
    )
  }, [])

  return {error, status, data, run}
}

function PokemonInfo({pokemonName}) {
  const {
    data: pokemon,
    status,
    error,
    run,
  } = useAsync({status: pokemonName ? 'pending' : 'idle'})

  React.useEffect(() => {
    if (!pokemonName) {
      return
    }
    // 💰 note the absence of `await` here. We're literally passing the promise
    // to `run` so `useAsync` can attach it's own `.then` handler on it to keep
    // track of the state of the promise.
    const pokemonPromise = fetchPokemon(pokemonName)
    run(pokemonPromise)
  }, [pokemonName, run])

...
  • It'd be way better if we redesigned our API a little bit by providing a run function that we are responsible for memoizing.

  • Now, useAsync no longer accepts a callback, and instead, it's just going to accept our initial state, and we'll get our state values here, and then we'll also get a run function, which we'll be able to call inside of our useEffect.

  • We're just going to accept the initial state, and we're no longer going to be doing this useEffect. Instead, we're going to make a function called run that is a useCallback function so that it's memoized.

  • Then, this run function is going to accept a promise, and then what is it going to do? It's going to do all of the exact same things that the original useEffect is doing.

  • What about dependencies for useCallback function in our app?

    • We are using this dispatch, but that is coming from useReducer, and ESLint plugin knows that this dispatch will never change. useReducer ensures that for us, we'll never get a new dispatch function, even as these functions are re-rendering, so we don't need to include dispatch in there. We can, if we want to, but it's not necessary. The other thing that we're using here is this promise, but we're accepting that as an argument, so that won't be a dependency that can change either. Our run function is solid. This will never change.

function useAsync(initialState) {
  const [state, unsafeDispatch] = React.useReducer(pokemonInfoReducer, {
    status: 'idle',
    data: null,
    error: null,
    ...initialState,
  })

  const {data, error, status} = state

  const mountedRef = React.useRef(false)

  React.useEffect(() => {
    mountedRef.current = true
    return () => (mountedRef.current = false)
  }, [])

  const dispatch = React.useCallback((...args) => {
    if (mountedRef.current) {
      unsafeDispatch(...args)
    }
  }, [])

  const run = React.useCallback(promise => {
    dispatch({type: 'pending'})
    promise.then(
      data => {
        dispatch({type: 'resolved', data})
      },
      error => {
        dispatch({type: 'rejected', error})
      },
    )
  }, [])

  return {error, status, data, run}
}
function useSafeDispatch(dispatch) {
  const mountedRef = React.useRef(false)

  React.useEffect(() => {
    mountedRef.current = true
    return () => (mountedRef.current = false)
  }, [])

  return React.useCallback(
    (...args) => {
      if (mountedRef.current) {
        dispatch(...args)
      }
    },
    [dispatch],
  )
}

Background

Memoization in general

Memoization: a performance optimization technique which eliminates the need to
recompute a value for a given input by storing the original computation and
returning that stored value when the same input is provided. Caching is a form
of memoization. Here's a simple implementation of memoization:

const values = {}
function addOne(num: number) {
  if (values[num] === undefined) {
    values[num] = num + 1 // <-- here's the computation
  }
  return values[num]
}

One other aspect of memoization is value referential equality. For example:

const dog1 = new Dog('sam')
const dog2 = new Dog('sam')
console.log(dog1 === dog2) // false

Even though those two dogs have the same name, they are not the same. However,
we can use memoization to get the same dog:

const dogs = {}
function getDog(name: string) {
  if (dogs[name] === undefined) {
    dogs[name] = new Dog(name)
  }
  return dogs[name]
}

const dog1 = getDog('sam')
const dog2 = getDog('sam')
console.log(dog1 === dog2) // true

You might have noticed that our memoization examples look very similar.
Memoization is something you can implement as a generic abstraction:

function memoize<ArgType, ReturnValue>(cb: (arg: ArgType) => ReturnValue) {
  const cache: Record<ArgType, ReturnValue> = {}
  return function memoized(arg: ArgType) {
    if (cache[arg] === undefined) {
      cache[arg] = cb(arg)
    }
    return cache[arg]
  }
}

const addOne = memoize((num: number) => num + 1)
const getDog = memoize((name: string) => new Dog(name))

Our abstraction only supports one argument, if you want to make it work for any
type/number of arguments, knock yourself out.

Memoization in React

Luckily, in React we don't have to implement a memoization abstraction. They
made two for us! useMemo and useCallback. For more on this read:
Memoization and React.

You know the dependency list of useEffect? Here's a quick refresher:

React.useEffect(() => {
  window.localStorage.setItem('count', count)
}, [count]) // <-- that's the dependency list

Remember that the dependency list is how React knows whether to call your
callback (and if you don't provide one then React will call your callback every
render). It does this to ensure that the side effect you're performing in the
callback doesn't get out of sync with the state of the application.

But what happens if I use a function in my callback?

const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
  updateLocalStorage()
}, []) // <-- what goes in that dependency list?

We could just put the count in the dependency list and that would
actually/accidentally work, but what would happen if one day someone were to
change updateLocalStorage?

- const updateLocalStorage = () => window.localStorage.setItem('count', count)
+ const updateLocalStorage = () => window.localStorage.setItem(key, count)

Would we remember to update the dependency list to include the key? Hopefully
we would. But this can be a pain to keep track of dependencies. Especially if
the function that we're using in our useEffect callback is coming to us from
props (in the case of a custom component) or arguments (in the case of a custom
hook).

Instead, it would be much easier if we could just put the function itself in the
dependency list:

const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
  updateLocalStorage()
}, [updateLocalStorage]) // <-- function as a dependency

The problem with that though it will trigger the useEffect to run every
render. This is because updateLocalStorage is defined inside the component
function body. So it's re-initialized every render. Which means it's brand new
every render. Which means it changes every render. Which means... you guessed
it, our useEffect callback will be called every render!

This is the problem useCallback solves. And here's how you solve it

const updateLocalStorage = React.useCallback(
  () => window.localStorage.setItem('count', count),
  [count], // <-- yup! That's a dependency list!
)
React.useEffect(() => {
  updateLocalStorage()
}, [updateLocalStorage])

What that does is we pass React a function and React gives that same function
back to us... Sounds kinda useless right? Imagine:

// this is not how React actually implements this function. We're just imagining!
function useCallback(callback) {
  return callback
}

Uhhh... But there's a catch! On subsequent renders, if the elements in the
dependency list are unchanged, instead of giving the same function back that we
give to it, React will give us the same function it gave us last time. So
imagine:

// this is not how React actually implements this function. We're just imagining!
let lastCallback
function useCallback(callback, deps) {
  if (depsChanged(deps)) {
    lastCallback = callback
    return callback
  } else {
    return lastCallback
  }
}

So while we still create a new function every render (to pass to useCallback),
React only gives us the new one if the dependency list changes.

In this exercise, we're going to be using useCallback, but useCallback is
just a shortcut to using useMemo for functions:

// the useMemo version:
const updateLocalStorage = React.useMemo(
  // useCallback saves us from this annoying double-arrow function thing:
  () => () => window.localStorage.setItem('count', count),
  [count],
)

// the useCallback version
const updateLocalStorage = React.useCallback(
  () => window.localStorage.setItem('count', count),
  [count],
)

🦉 A common question with this is: "Why don't we just wrap every function in
useCallback?" You can read about this in my blog post
When to useMemo and useCallback.

🦉 And if the concept of a "closure" is new or confusing to you, then
give this a read. (Closures are one of the reasons
it's important to keep dependency lists correct.)

Exercise

Production deploys:

People tend to find this exercise more difficult, so I strongly advise
spending some time understanding how the code works before making any changes!

Also, one thing to keep in mind is that React hooks are a great foundation upon
which to build libraries and many have been built. For that reason, you don't
often need to go this deep into making custom hooks. So if you find this one
isn't clicking for you, know that you are learning and when you do face a
situation when you need to use this knowledge, you'll be able to come back and
it will click right into place.

👨‍💼 Peter the Product Manager told us that we've got more features coming our way
that will require managing async state. We've already got some code for our
pokemon lookup feature (if you've gone through the "React Hooks" workshop
already, then this should be familiar, if not, spend some time playing with the
app to get up to speed with what we're dealing with here). We're going to
refactor out the async logic so we can reuse this in other areas of the app.

So, your job is to extract the logic from the PokemonInfo component into a
custom and generic useAsync hook. In the process you'll find you need to do
some fancy things with dependencies (dependency arrays are the biggest challenge
to deal with when making custom hooks).

NOTE: In this part of the exercise, we don't need useCallback. We'll add it in
the extra credits. It's important that you work on this refactor first so
you can appreciate the value useCallback provides in certain circumstances.

Extra Credit

1. 💯 use useCallback to empower the user to customize memoization

Production deploy

Unfortunately, the ESLint plugin is unable to determine whether the
dependencies argument is a valid argument for useEffect which is a shame,
and normally I'd say just ignore it and move on. But, there's another solution
to this problem which I think is probably better.

Instead of accepting dependencies to useAsync, why don't we just treat the
asyncCallback as a dependency? Any time asyncCallback changes, we know that
we should call it again. The problem is that because our asyncCallback depends
on the pokemonName which comes from props, it has to be defined within the
body of the component, which means that it will be defined on every render which
means it will be new every render. This is where React.useCallback comes in!

Here's another example of the React.useCallback API:

function ConsoleGreeting(props) {
  const greet = React.useCallback(
    greeting => console.log(`${greeting} ${props.name}`),
    [props.name],
  )

  React.useEffect(() => {
    const helloGreeting = 'Hello'
    greet(helloGreeting)
  }, [greet])
  return <div>check the console</div>
}

The first argument to useCallback is the callback you want called, the second
argument is an array of dependencies which is similar to useEffect. When one
of the dependencies changes between renders, the callback you passed in the
first argument will be the one returned from useCallback. If they do not
change, then you'll get the callback which was returned the previous time (so
the callback remains the same between renders).

So we only want our asyncCallback to change when the pokemonName changes.
See if you can make things work like this:

// 🐨 you'll need to wrap asyncCallback in React.useCallback
function asyncCallback() {
  if (!pokemonName) {
    return
  }
  return fetchPokemon(pokemonName)
}

// 🐨 you'll need to update useAsync to remove the dependencies and list the
// async callback as a dependency.
const state = useAsync(asyncCallback)

2. 💯 return a memoized run function from useAsync

Production deploy

Requiring users to provide a memoized value is fine. You can document it as part
of the API and expect people to just read the docs right? lol, that's hilarious
😂 It'd be WAY better if we could redesign the API a bit so we (as the hook
developers) are the ones who have to memoize the function, and the users of our
hook don't have to worry about it.

So see if you can redesign this a little bit by providing a (memoized) run
function that people can call in their own useEffect like this:

// 💰 destructuring this here now because it just felt weird to call this
// "state" still when it's also returning a function called "run" 🙃
const {data: pokemon, status, error, run} = useAsync({ status: pokemonName ? 'pending' : 'idle' })

React.useEffect(() => {
  if (!pokemonName) {
    return
  }
  // 💰 note the absence of `await` here. We're literally passing the promise
  // to `run` so `useAsync` can attach it's own `.then` handler on it to keep
  // track of the state of the promise.
  const pokemonPromise = fetchPokemon(pokemonName)
  run(pokemonPromise)
}, [pokemonName, run])

3. 💯 make safeDispatch with useCallback, useRef, and useEffect

Production deploy

NOTICE: Things have changed slightly. The app you're running the exercises
in was changed since the videos were recorded and you can no longer see this
issue by changing the exercise. All the exercises are now rendered in an iframe
on the exercise pages, so when you go to a different exercise, you're
effectively "closing" the page, so all JS execution for that exercise stops.

So I've added a little checkbox which you can use to mount and unmount the
component with ease. This has the benefit of also working on the isolated page
as well. On the exercise page, you'll want to make sure that your console output
is showing the output from the iframe by
selecting the right context.

I've also added a test for this one to help make sure you've got it right.

Also notice that while what we're doing here is still useful and you'll learn
valuable skills, the warning we're suppressing
goes away in React v18.

Phew, ok, back to your extra credit!

This one's a bit tricky, and I'm going to be intentionally vague here to give
you a bit of a challenge, but consider the scenario where we fetch a pokemon,
and before the request finishes, we change our mind and navigate to a different
page (or uncheck the mount checkbox). In that case, the component would get
removed from the page ("unmounted") and when the request finally does complete,
it will call dispatch, but because the component has been removed from the
page, we'll get this warning from React:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

The best solution to this problem would be to
cancel the request,
but even then, we'd have to handle the error and prevent the dispatch from
being called for the rejected promise.

So see whether you can work out a solution for preventing dispatch from being
called if the component is unmounted. Depending on how you implement this, you
might need useRef, useCallback, and useEffect.

🦉 Other notes

useEffect and useCallback

The use case for useCallback in the exercise is a perfect example of the types
of problems useCallback is intended to solve. However the examples in these
instructions are intentionally contrived. You can simplify things a great deal
by not extracting code from useEffect into functions that you then have to
memoize with useCallback. Read more about this here:
Myths about useEffect.

useCallback use cases

The entire purpose of useCallback is to memoize a callback for use in
dependency lists and props on memoized components (via React.memo, which you
can learn more about from the performance workshop). The only time it's useful
to use useCallback is when the function you're memoizing is used in one of
those two situations.

🦉 Feedback

Fill out
the feedback form.

useReducer: simple Counter

useReducer: simple Counter

📝 Your Notes

Exercise

import * as React from 'react'

function countReducer(state, newState) {
  return newState
}

function Counter({initialCount = 0, step = 1}) {
  // 🐨 replace React.useState with React.useReducer.
  // 💰 React.useReducer(countReducer, initialCount)
  const [count, setCount] = React.useReducer(countReducer, initialCount)

  // 💰 you can write the countReducer function so you don't have to make any
  // changes to the next two lines of code! Remember:
  // The 1st argument is called "state" - the current value of count
  // The 2nd argument is called "newState" - the value passed to setCount
  const increment = () => setCount(count + step)
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

export default App
  • We want to keep in mind that the first argument to our reducer is going to be our state.

  • The second argument is called the action. Action is whatever is passed to our updater function. This setCount function, which is referred to as the dispatch function. Whatever is passed to this dispatch function will be the second argument to our reducer. We can call this the new state.

  • If we're getting the new value we want for the state, we need to return the new value we want for the state. Oh. That's interesting. Let's just return the new state.

  • We learned a couple of things here. We have useReducer. The first argument is a reducer function that accepts the current state and then an action or whatever the dispatch function is called with. The second argument here is the initial value for our state, which happens to be a number.

  • Our count is a number, and setCount is going to be called with some value that we want to update the state to. That's exactly what we're doing here in our countReducer is we just ignore that current state, and we just return that new state. That's what our state will get updated to.

1. 💯 accept the step as the action

import * as React from 'react'

function countReducer(count, step) {
  return count + step
}

function Counter({initialCount = 0, step = 1}) {
  const [count, changeCount] = React.useReducer(countReducer, initialCount)
  const increment = () => changeCount(step)
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

export default App
  • Now, we've called that state stillCount, but our dispatch function we're calling changeCount, and so we want to change the count by this step. All right. We can do that.
  • Our increment function we'll just call changeCount with this step, so now this is going to be step rather than our new state. Now, we need to return the new state, and it turns out that this is the count that's our current state. We can return the count plus the step, and that will get things working splendidly.
  • All we did here was we updated this dispatch function from setCount to changeCount. Then when we call it, we specify how much we want to change the count by. Because our count reducer will receive the current value of the count, we're able to combine that current value of the count with the step that was passed. That's going to get us our new state.

2. 💯 simulate setState with an object

import * as React from 'react'

function countReducer(state, action) {
  return {...state, ...action}
}

function Counter({initialCount = 0, step = 1}) {
  const [state, setState] = React.useReducer(countReducer, {
    count: initialCount,
  })
  const {count} = state
  const increment = () => setState({count: count + step})
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

export default App
  • The change now is no longer the change for how much the count should be incremented by, but instead, it's something more like the new state. Technically, it's referred to as an action.
  • Then what we need to do is take that existing state that we have and combine it with the action, which is the value that's passed to our dispatch function here. That'll get us our new state, and then we return that value.
  • What I'm going to do is, we'll just return an object that is a combination of that state and that action. Any properties the action has will override the properties in the state. That will simulate our state and setState from our good old class components.

.3. 💯 simulate setState with an object OR function

import * as React from 'react'

const countReducer = (state, action) => ({
  ...state,
  ...(typeof action === 'function' ? action(state) : action),
})

function Counter({initialCount = 0, step = 1}) {
  const [state, setState] = React.useReducer(countReducer, {
    count: initialCount,
  })
  const {count} = state
  const increment = () =>
    setState(currentState => ({count: currentState.count + step}))
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

export default App
  • What I'm going to do is I'm going to add an expression here that will say, "Type of action is a function." If it is a function, then we'll call the action with the state, and we'll spread to that value. Otherwise, we are just going to assume it's an object. We'll say action, and we'll spread that value.

4. 💯 traditional dispatch object with a type and switch statement

import * as React from 'react'

function countReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      // return new state value
      return {count: state.count + action.step}
    default:
      throw new Error(`Unsupported action type: ${action.type}`)
  }
}

function Counter({initialCount = 0, step = 1}) {
  const [state, dispatch] = React.useReducer(countReducer, {
    count: initialCount,
  })
  const {count} = state
  const increment = () => dispatch({type: 'INCREMENT', step})
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

export default App

Background

React's useState hook can get you a really long way with React state
management. That said, sometimes you want to separate the state logic from the
components that make the state changes. In addition, if you have multiple
elements of state that typically change together, then having an object that
contains those elements of state can be quite helpful.

This is where useReducer comes in really handy. If you're familiar with redux,
then you'll feel pretty comfortable here. If not, then you have less to unlearn
😉

This exercise will take you pretty deep into useReducer. Typically, you'll use
useReducer with an object of state, but we're going to start by managing a
single number (a count). We're doing this to ease you into useReducer and
help you learn the difference between the convention and the actual API.

Here's an example of using useReducer to manage the value of a name in an
input.

function nameReducer(previousName, newName) {
  return newName
}

const initialNameValue = 'Joe'

function NameInput() {
  const [name, setName] = React.useReducer(nameReducer, initialNameValue)
  const handleChange = event => setName(event.target.value)
  return (
    <>
      <label>
        Name: <input defaultValue={name} onChange={handleChange} />
      </label>
      <div>You typed: {name}</div>
    </>
  )
}

One important thing to note here is that the reducer (called nameReducer
above) is called with two arguments:

  1. the current state
  2. whatever it is that the dispatch function (called setName above) is called
    with. This is often called an "action."

Exercise

Production deploys:

We're going to start off as simple as possible with a <Counter /> component.
useReducer is absolutely overkill for a counter component like ours, but for
now, just focus on making things work with useReducer.

📜 Here are two really helpful blog posts comparing useState and useReducer:

Extra Credit

1. 💯 accept the step as the action

Production deploy

I want to change things a bit to have this API:

const [count, changeCount] = React.useReducer(countReducer, initialCount)
const increment = () => changeCount(step)

How would you need to change your reducer to make this work?

This one is just to show that you can pass anything as the action.

2. 💯 simulate setState with an object

Production deploy

Remember this.setState from class components? If not, lucky you 😉. Either
way, let's see if you can figure out how to make the state updater (dispatch
function) behave in a similar way by changing our state to an object
({count: 0}) and then calling the state updater with an object which merges
with the current state.

So here's how I want things to look now:

const [state, setState] = React.useReducer(countReducer, {
  count: initialCount,
})
const {count} = state
const increment = () => setState({count: count + step})

How would you need to change the reducer to make this work?

3. 💯 simulate setState with an object OR function

Production deploy

this.setState from class components can also accept a function. So let's add
support for that with our simulated setState function. See if you can figure
out how to make your reducer support both the object as in the last extra credit
as well as a function callback:

const [state, setState] = React.useReducer(countReducer, {
  count: initialCount,
})
const {count} = state
const increment = () =>
  setState(currentState => ({count: currentState.count + step}))

4. 💯 traditional dispatch object with a type and switch statement

Production deploy

Ok, now we can finally see what most people do conventionally (mostly thanks to
redux). Update your reducer so I can do this:

const [state, dispatch] = React.useReducer(countReducer, {
  count: initialCount,
})
const {count} = state
const increment = () => dispatch({type: 'INCREMENT', step})

🦉 Other notes

lazy initialization

This one's not an extra credit, but sometimes lazy initialization can be
useful, so here's how we'd do that with our original hook App:

function init(initialStateFromProps) {
  return {
    pokemon: null,
    loading: false,
    error: null,
  }
}

// ...

const [state, dispatch] = React.useReducer(reducer, props.initialState, init)

So, if you pass a third function argument to useReducer, it passes the second
argument to that function and uses the return value for the initial state.

This could be useful if our init function read into localStorage or something
else that we wouldn't want happening every re-render.

The full useReducer API

If you're into TypeScript, here's some type definitions for useReducer:

Thanks to Trey's blog post

Please don't spend too much time reading through this by the way!

type Dispatch<A> = (value: A) => void
type Reducer<S, A> = (prevState: S, action: A) => S
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
  ? S
  : never
type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<
  any,
  infer A
>
  ? A
  : never

function useReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  initializerArg: I & ReducerState<R>,
  initializer: (arg: I & ReducerState<R>) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>]

function useReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  initializerArg: I,
  initializer: (arg: I) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>]

function useReducer<R extends Reducer<any, any>>(
  reducer: R,
  initialState: ReducerState<R>,
  initializer?: undefined,
): [ReducerState<R>, Dispatch<ReducerAction<R>>]

useReducer is pretty versatile. The key takeaway here is that while
conventions are useful, understanding the API and its capabilities is more
important.

🦉 Feedback

Fill out
the feedback form.

Forms

Forms

📝 Your Notes

Kent

import * as React from 'react'

function UsernameForm({onSubmitUsername}) {
  // 🐨 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
  // events (which refreshes the page).
  // 📜 https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault
  //
  // 🐨 get the value from the username input (using whichever method
  // you prefer from the options mentioned in the instructions)
  // 💰 For example: event.target.elements[0].value
  // 🐨 Call `onSubmitUsername` with the value of the input

  // 🐨 add the onSubmit handler to the <form> below

  // 🐨 make sure to associate the label to the input.
  // to do so, set the value of 'htmlFor' prop of the label to the id of input
  function handleSubmit(event) {
    event.preventDefault()
    const value = event.target.elements.usernameInput.value
    onSubmitUsername(value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="usernameInput">Username:</label>
        <input id="usernameInput" type="text" />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

function App() {
  const onSubmitUsername = username => alert(`You entered: ${username}`)
  return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

export default App
  • Why use preventDefault?

    • What's happening is the browser, by default, when a form is submitted, will do a post request to the current URL with the values of the form.

    • image

    • Now, we just have a question mark. We don't have any values of the form. The reason for that is because our input is not named. If I give it the name of username and save that. Then we'll come over here and say something. We'll hit Submit. We'll get a full page refresh. We'll get username equals something.

    • image

    • image

    • This is the default behavior for the browser, but we're making a single-page application with React. We don't want to do a full page refresh. We don't need to make a post to the current URL. We're going to do all of this stuff in JavaScript. That's why we're going to accept the event that is our submit event. We're going to say event.preventDefault().

  • What a devil is a synthetic event?

    • Synthetic event is actually not a real event from the browser. It's an object that React creates for us that looks and behaves exactly like a regular event.
    • Most of the time, you won't know that you're interacting with a fake event or a SyntheticEvent from React. You'll just assume that you're interacting with the native event. They do this for performance reasons. React also uses event delegation, if that's something that you've heard of, all to just improve the performance of your application.
    • If you ever need to access the native event, then you can do so with .nativeEvent. We'll save that, hit submit, and now we get our submit event. That's the actual event object that was triggered when I click that Submit button. Again, you won't notice the difference normally.
  • Why use id in an input element?

    • The reason that I want to use ID instead is because I should be labeling my input, and even though the label appears right next to it, and many users will just make sense of that, screen readers need this label to be associated to that input to work properly.
    • In addition, if it's not labeled properly, then when I click on the label, it does not focus the input, which is actually what I want.
    • We're going to say htmlFor username input. This is saying, "Hey, this label is for username input." Now, in HTML, this attribute is actually just the four attribute, but the DOM property name for the label is htmlFor.
    • That's why you say htmlFor. You pass an ID to the thing that you're trying to label and that associates the label to the input. If we save that now, we click on the username, and that will focus on that input.
    • In addition, we can now access our username input under the elements property of our form. Now, we can say username input and get the value. Let's do it that way. We're going to say event.target.elements.usernameinput, and that's going to get us our value.

1. 💯 using refs

import * as React from 'react'

function UsernameForm({onSubmitUsername}) {
  const usernameInputRef = React.useRef(null)

  function handleSubmit(event) {
    event.preventDefault()
    const value = usernameInputRef.current.value
    onSubmitUsername(value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="usernameInput">Username:</label>
        <input id="usernameInput" type="text" ref={usernameInputRef} />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

function App() {
  const onSubmitUsername = username => alert(`You entered: ${username}`)
  return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

export default App

2. 💯 Validate lower-case

import * as React from 'react'

function UsernameForm({onSubmitUsername}) {
  const [error, setError] = React.useState(null)

  function handleChange(event) {
    const {value} = event.target
    const isLowerCase = value === value.toLowerCase()

    setError(isLowerCase ? null : 'Username must be lower case')
  }

  return (
    <form>
      <div>
        <label htmlFor="usernameInput">Username:</label>
        <input id="usernameInput" type="text" onChange={handleChange} />
      </div>
      <button type="submit" disabled={Boolean(error)}>
        Submit
      </button>
      {error && <p style={{color: 'red'}}>{error}</p>}
    </form>
  )
}

function App() {
  const onSubmitUsername = username => alert(`You entered: ${username}`)
  return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

export default App
  • We determined whether or not it's lowercase. Then we set the error state based on whether it's lowercase. If it is lowercase, then we just set it to null, so there's no error. If it's not lowercase, then we'll set it to this string, and we got that setError by using the useState hook from React, which will just maintain some state for us over time.
  • It gives us the current state, and it gives us a mechanism for updating that state. Based on that state, we're going to set the disabled value of the Submit button, and we're going to render the error if there is an error there.

3. 💯 Control the input value

import * as React from 'react'

function UsernameForm({onSubmitUsername}) {
  const [username, setUsername] = React.useState('')

  function handleChange(event) {
    const {value} = event.target
    setUsername(value.toLowerCase())
  }

  function handleSubmit(event) {
    event.preventDefault()
    onSubmitUsername(username)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="usernameInput">Username:</label>
        <input
          id="usernameInput"
          type="text"
          onChange={handleChange}
          value={username}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

function App() {
  const onSubmitUsername = username => alert(`You entered: ${username}`)
  return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

export default App
  • I'm going to make another you state here for the username. We'll have setUserName. We'll initialize that to an empty string. We want this input to start out as an empty string, which is its default behavior already.
  • If you ever need to programmatically change the input value from what the user is typing, whether you click checkbox and that pre-fills some form input, or whatever the case may be, doing a lowercase, even though the user's typing uppercase characters, the way that you do that is using the value prop. This is called a controlled input.

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.

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 <form> DOM node which has a reference to the elements of the
form which can be used to get the values out of the form!

Exercise

Production deploys:

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

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.

2. 💯 Validate lower-case

Production deploy

With React, the way you use state is via a special "hook" called useState.
Here's a simple example of what that looks like:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(count + 1)
  return <button onClick={increment}>{count}</button>
}

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

In this extra credit, 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.

Once we have that wired up then we can render the error message and disable the
submit button if there's an error.

💰 This one's a little more tricky, so here are a few things you need to do to
make this work:

  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

You may consider adding a role="alert" to the element you use to display the
error to assist with screen reader users.

Make sure you pass handleChange to the onChange handler of the input.

3. 💯 Control the input value

Production deploy

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:

<input value={myInputValue} />

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 extra credit 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.

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.

useLayoutEffect: auto-scrolling textarea

useLayoutEffect: auto-scrolling textarea

📝 Your Notes

useEffect

  • 99% of the time this is what you want to use. When hooks are stable and if you refactor any of your class components to use hooks, you'll likely move any code from componentDidMount, componentDidUpdate, and componentWillUnmount to useEffect.
  • The one catch is that this runs after react renders your component and ensures that your effect callback does not block browser painting. This differs from the behavior in class components where componentDidMount and componentDidUpdate run synchronously after rendering. It's more performant this way and most of the time this is what you want.
  • However, if your effect is mutating the DOM (via a DOM node ref) and the DOM mutation will change the appearance of the DOM node between the time that it is rendered and your effect mutates it, then you don't want to use useEffect. You'll want to use useLayoutEffect. Otherwise the user could see a flicker when your DOM mutations take effect. This is pretty much the only time you want to avoid useEffect and use useLayoutEffect instead.

useLayoutEffect

  • This runs synchronously immediately after React has performed all DOM mutations. This can be useful if you need to make DOM measurements (like getting the scroll position or other styles for an element) and then make DOM mutations or trigger a synchronous re-render by updating state.
  • As far as scheduling, this works the same way as componentDidMount and componentDidUpdate. Your code runs immediately after the DOM has been updated, but before the browser has had a chance to "paint" those changes (the user doesn't actually see the updates until after the browser has repainted).

Background

There are two ways to tell React to run side-effects after it renders:

  1. useEffect
  2. useLayoutEffect

The difference about these is subtle (they have the exact same API), but
significant. 99% of the time useEffect is what you want, but sometimes
useLayoutEffect can improve your user experience.

To learn about the difference, read
useEffect vs useLayoutEffect

And check out the hook flow diagram as
well.

Exercise

Production deploys:

NOTE: React 18 has smoothed out the differences in the UX between useEffect
and useLayoutEffect. That said, the simple "rule" described still applies!

There's no exercise for this one because basically you just need to replace
useEffect with useLayoutEffect and you're good. So you pretty much just need
to experiment with things a bit.

Before you do that though, compare the finished example with the exercise.
Add/remove messages and you'll find that there's a janky experience with the
exercise version because we're using useEffect and there's a gap between the
time that the DOM is visually updated and our code runs.

Here's the simple rule for when you should use useLayoutEffect: If you are
making observable changes to the DOM, then it should happen in
useLayoutEffect, otherwise useEffect.

🦉 Feedback

Fill out
the feedback form.

Routing

Routing

📝 Your Notes

Elaborate on your learnings here in INSTRUCTIONS.md

Background

The URL is arguably one of the best features of the web. The ability for one
user to share a link to another user who can use it to go directly to a piece of
content on a given website is fantastic. Other platforms don't really have this.

The de-facto standard library for routing React applications is
React Router. It's terrific.

The idea behind routing on the web is you have some API that informs you of
changes to the URL, then you react (no pun intended) to those changes by
rendering the correct user interface based on that URL route. In addition, you
can change the URL when the user performs an action (like clicking a link or
submitting a form). This all happens client-side and does not reload the
browser.

Here's a quick demo of a few of the features you'll need to know about for this
exercise:

import * as React from 'react'
import ReactDOM from 'react-dom'
import {
  BrowserRouter as Router,
  Routes,
  Route,
  useParams,
  Link,
} from 'react-router-dom'

function Home() {
  return <h2>Home</h2>
}

function About() {
  return <h2>About</h2>
}

function Dog() {
  const params = useParams()
  const {dogId} = params
  return <img src={`/img/dogs/${dogId}`} />
}

function Nav() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/dog/123">My Favorite Dog</Link>
    </nav>
  )
}

function YouLost() {
  return <div>You lost?</div>
}

function App() {
  return (
    <div>
      <h1>Welcome</h1>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dog/:dogId" element={<Dog />} />
        <Route path="*" element={<YouLost />} />
      </Routes>
    </div>
  )
}

function AppWithRouter() {
  return (
    <Router>
      <App />
    </Router>
  )
}

ReactDOM.render(<AppWithRouter />, document.getElementById('app'))

That should be enough to get you going on this exercise.

As a fun exercise in routing on the web, you can read this blog post
demonstrating how you might build your own React Router (version 4):
https://ui.dev/build-your-own-react-router/

Exercise

Production deploys:

👨‍💼 Users want to be able to click on the book in the search results and be taken
to a special page for that book. We want the URL to be /book/:bookId. Oh, and
if the user lands on a page that we don't have a route for, then we should show
them a nice message and a link to take them back home.

And we want to have a nav bar on the left. There's really only one link we'll
have in there right now, but we'll put more in there soon.

Here are the URLs we should support:

/discover       ->     Discover books screen
/book/:bookId   ->     Book screen
*               ->     Helpful "not found" screen

💰 tip: there's no need to render the router around the Unauthenticated App,
just the Authenticated one.

Files

  • src/app.js
  • src/authenticated-app.js
  • src/components/book-row.js
  • src/screens/not-found.js
  • src/screens/book.js

Extra Credit

1. 💯 handle URL redirects

Production deploy

We don't have anything to show at the home route /. We should redirect the
user from that route to /discover automatically. Often, developers will use
their client-side router to redirect users, but it's not possible for the
browser and search engines to get the proper status codes for redirect (301
or 302) so that's not optimal. The server should be configured to handle those.

There are three environments where we have a server serving our content:

  1. Locally during development
  2. Locally when running the built code via the npm run serve script which uses
    https://npm.im/serve
  3. In production with Netlify

So for this extra credit you need to configure each of these to redirect / to
/discover.

Local Development

We need to add redirect functionality to the webpack server that react-scripts
is running for us. We can do that with the ./src/setupProxy.js file. In that
file we export a function that accepts an
express app and attaches a get to handle requests
sent to a URL matching this regex: /\/$/ and redirects to /discover.

function proxy(app) {
  // add the redirect handler here
}

module.exports = proxy

📜 Here are some docs that might be helpful to you:

With serve

The serve module can be configured with a serve.json file in the directory
you serve. Open ./public/serve.json and see if you can figure out how to get
that to redirect properly.

To know whether it worked, you'll need to run:

npm run build
npm run serve

Then open http://localhost:8811. It worked if your redirected to
http://localhost:8811/discover.

📜 Here are the docs you'll probably want:

In production

There are a few ways to configure Netlify to do redirects. We'll use the
_redirects file. Open ./public/_redirects and add support for redirecting
/ to /discover.

There's no easy way to test this works, so just compare your solution to the
final file and take my word for it that it works. Or, if you really want to
check it out, you can run npm run build and then drag and drop the build
directory here: https://app.netlify.com/drop

📜 Here's the docs for Netlify's _redirects file:

Hint: you need to use the "!" force feature for this.

For more on why we prefer server-side redirects over client-side, read
Stop using client-side route redirects.

Files:

  • src/setupProxy.js
  • public/serve.json
  • public/_redirects

2. 💯 add useMatch to highlight the active nav item

Production deploy

This isn't quite as useful right now, but when we've got several other links in
the nav, it will be helpful to orient users if we have some indication as to
which link the user is currently viewing. Our designer has given us this CSS you
can use:

{
  borderLeft: `5px solid ${colors.indigo}`,
  background: colors.gray10,
  ':hover': {
    background: colors.gray20,
  },
}

You can determine whether the URL matches a given path via the useMatch hook:

const matches = useMatch('/some-path')

From there, you can conditionally apply the styles.

💰 Tip: the Link component in NavLink already has a CSS prop on it. The easiest
way to add these additional styles is by passing an array to the css prop like
so:

<div
  css={[
    {
      /* styles 1 */
    },
    {
      /* styles 2 */
    },
  ]}
/>

Files:

  • src/authenticated-app.js

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you've just learned, then
fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Build%20React%20Apps&e=05%3A%20Routing&em=

Styling

Styling

📝 Your Notes

Kent & mine

import * as React from 'react'
import '../box-styles.css'

// 🐨 add a className prop to each div and apply the correct class names
// based on the text content
// 💰 Here are the available class names: box, box--large, box--medium, box--small
// 💰 each of the elements should have the "box" className applied

// 🐨 add a style prop to each div so their background color
// matches what the text says it should be
// 🐨 also use the style prop to make the font italic
// 💰 Here are available style attributes: backgroundColor, fontStyle

const smallBox = (
  <div
    className="box box--small"
    style={{fontStyle: 'italic', backgroundColor: 'lightBlue'}}
  >
    small lightblue box
  </div>
)
const mediumBox = (
  <div
    className="box box--medium"
    style={{fontStyle: 'italic', backgroundColor: 'pink'}}
  >
    medium pink box
  </div>
)
const largeBox = (
  <div
    className="box box--large"
    style={{fontStyle: 'italic', backgroundColor: 'orange'}}
  >
    large orange box
  </div>
)

function App() {
  return (
    <div>
      {smallBox}
      {mediumBox}
      {largeBox}
    </div>
  )
}

export default App
  • We have the className gets converted into a class, because this is the DOM property, which is represented by the attribute. The DOM property is className. Let's take a look at that really quickly.

  • I'll pull up the console here. When we have this selected, you'll see that $ right there. If you do $ right here, then you'll reference the thing you have selected, which is pretty cool. Then with $, we can say .className. Look, it's that string. If you do a .class, you get nothing. The class attribute in HTML is converted to the className property, which is why the className prop is what we set in our JSX.

  • image

  • image

  • Similarly, if you do a $.style, we get a CSSStyleDeclaration, which is an object of a ton of stuff, pretty much everything that you can style in here. We have the font style and background color, the things that are set.

  • image

  • Even though in the HTML it shows up as a string of CSS, in the DOM this style is an object. The className and style here represent the DOM properties rather than the HTML attributes, which is interesting.

Extra 1

import * as React from 'react'
import '../box-styles.css'

// to prevent undefined
function Box({className = '', style, ...otherProps}) {
  return (
    <div className={`box ${className}`} style={{fontStyle: 'italic', ...style}} {...otherProps} />
  )
}

const smallBox = (
  <Box
    id="small-box"
    className="box--small"
    style={{backgroundColor: 'lightBlue'}}
  >
    small lightblue box
  </Box>
)

const mediumBox = (
  <Box className="box--medium" style={{backgroundColor: 'pink'}}>
    medium pink box
  </Box>
)

const largeBox = (
  <Box className="box--large" style={{backgroundColor: 'orange'}}>
    large orange box
  </Box>
)

function App() {
  return (
    <>
      {smallBox}
      {mediumBox}
      {largeBox}
      <Box>sizeless box</Box>
    </>
  )
}

export default App

Extra 2

import * as React from 'react'
import '../box-styles.css'

function Box({style, size, className, ...otherProps}) {
  const sizeClassName = size ? `box--${size}` : ''

  return (
    <div
      className={`box ${className} ${sizeClassName}`}
      style={{fontStyle: 'italic', ...style}}
      {...otherProps}
    />
  )
}

const smallBox = (
  <Box id="small-box" size="small" style={{backgroundColor: 'lightBlue'}}>
    small lightblue box
  </Box>
)

const mediumBox = (
  <Box size="medium" style={{backgroundColor: 'pink'}}>
    medium pink box
  </Box>
)

const largeBox = (
  <Box size="large" style={{backgroundColor: 'orange'}}>
    large orange box
  </Box>
)

function App() {
  return (
    <>
      {smallBox}
      {mediumBox}
      {largeBox}
      <Box>sizeless box</Box>
    </>
  )
}

export default App

Background

There are two primary ways to style react components

  1. Inline styles with the style prop
  2. Regular CSS with the className prop

About the style prop:

  • In HTML you'd pass a string of CSS:
<div style="margin-top: 20px; background-color: blue;"></div>
  • In React, you'll pass an object of CSS:
<div style={{marginTop: 20, backgroundColor: 'blue'}} />

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:

const myStyles = {marginTop: 20, backgroundColor: 'blue'}
<div style={myStyles} />

Note also that the property names are camelCased rather than kebab-cased.
This matches the style property of DOM nodes (which is a
CSSStyleDeclaration
object).

About the className prop:

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.

Exercise

Production deploys:

In this exercise we'll use both methods for styling react components.

We have the following css on the page:

.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;
}

Your job is to apply the right className and style props to the divs so the
styles applied match the text content.

Extra Credit

1. 💯 Create a custom component

Production deploy

Try to make a custom <Box /> component that renders a div, accepts all the
props and merges the given style and className props with the shared values.

I should be able to use it like so:

<Box className="box--small" style={{backgroundColor: 'lightblue'}}>
  small lightblue box
</Box>

The box className and fontStyle: 'italic' style should be applied in
addition to the values that come from props.

2. 💯 accept a size prop to encapsulate styling

Production deploy

It's great that we're composing the classNames and styles 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.

In this extra credit, try to make this API work:

<Box size="small" style={{backgroundColor: 'lightblue'}}>
  small lightblue box
</Box>

Attribution

Matt Zabriskie developed this example
originally for
a workshop we gave together.

🦉 Feedback

Fill out
the feedback form.

Using JSX

Using JSX

📝 Your Notes

Kent

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>

  <script type="text/babel">
    const element = <div className="container">Hello World</div>;
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Mine

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

  <!-- 🐨 add Babel to the page.
         💰 Here is the script tag that'll do the job:
    -->
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>

  <script type="text/babel">
    // 🐨 on the script tag above, change `type="module"`
    // to `type="text/babel"` so babel will compile this code for the browser to run.

    // 🐨 re-implement this using JSX!
    // const element = React.createElement('div', {
    //   className: 'container',
    //   children: 'Hello World',
    // })
    const element = <div className="container">Hello World</div>;

    // 💰 there are a few subtle differences between JSX and HTML. One such
    // difference is how you apply a class to an element in JSX is by using
    // `className` rather than `class`!
    // 📜 You can learn the differences between JSX and HTML syntax from the React docs here:
    // https://reactjs.org/docs/dom-elements.html#differences-in-attributes

    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Babel makes the compiled code in Tag

image

Extra 1

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>

  <script type="text/babel">
    // const element = <div className="container">Hello World</div>;
    const className = 'container'
    const children = 'Hello World'
    const element = <div className={className}>{children}</div>
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Extra 2

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>

  <script type="text/babel">
    const children = 'Hello World'
    const className = 'container'
    const props = { children, className }
    const element = <div {...props} />
    ReactDOM.createRoot(document.getElementById('root')).render(element)
  </script>
</body>

Spread operator is just syntax sugar

"use strict";

function _extends() {
  _extends =
    Object.assign ||
    function (target) {
      for (var i = 1; i < arguments.length; i++) {
        var source = arguments[i]
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key]
          }
        }
      }
      return target
    }
  return _extends.apply(this, arguments)
}

var children = 'Hello World';
var className = 'container';
var props = {
  children: children,
  className: className
};
var element = /*#__PURE__*/React.createElement("div", _extends({
  id: "my-thing"
}, props));
ReactDOM.createRoot(document.getElementById('root')).render(element);

Background

JSX is more intuitive than the raw React API and is easier to understand when
reading the code. It's fairly simple HTML-like syntactic sugar on top of the raw
React APIs:

const ui = <h1 id="greeting">Hey there</h1>

// ↓ ↓ ↓ ↓ compiles to ↓ ↓ ↓ ↓

const ui = React.createElement('h1', {id: 'greeting', children: 'Hey there'})

Because JSX is not actually JavaScript, you have to convert it using something
called a code compiler. Babel is one such tool.

🦉 Pro tip: If you'd like to see how JSX gets compiled to JavaScript,
check out the online babel REPL here.

If you can train your brain to look at JSX and see the compiled version of that
code, you'll be MUCH more effective at reading and using it! I strongly
recommend you give this some intentional practice.

Exercise

Production deploys:

Normally you'll compile all of your code at build-time before you ship your
application to the browser, but because Babel is written in JavaScript we can
actually run it in the browser to compile our code on the fly and that's what
we'll do in this exercise.

So we'll include a script tag for Babel, then we'll update our own script tag to
tell Babel to compile it for us on the fly. When you're done, you should notice
the compiled version of the code appears in the <head> of the DOM (which you
can inspect using DevTools).

Extra Credit

1. 💯 interpolate className and children

Production deploy

"Interpolation" is defined as "the insertion of something of a different nature
into something else."

Let's take template literals for example:

const greeting = 'Sup'
const subject = 'World'
const message = `${greeting} ${subject}`

See if you can figure out how to extract the className ("container") and
children ("Hello World") to variables and interpolate them in the JSX.

const className = 'container'
const children = 'Hello World'
const element = <div className="hmmm">how do I make this work?</div>

📜 The react docs for JSX are pretty good:
https://reactjs.org/docs/introducing-jsx.html

Here are a few sections of particular interest for this extra credit:

2. 💯 spread props

Production deploy

What if I have an object of props that I want applied to the div like this:

const children = 'Hello World'
const className = 'container'
const props = {children, className}
const element = <div /> // how do I apply props to this div?

If we were doing raw React APIs it would be:

const element = React.createElement('div', props)

Or, it could be written like this:

const element = React.createElement('div', {...props})

See if you can figure out how to make that work with JSX.

📜 https://reactjs.org/docs/jsx-in-depth.html#spread-attributes

🦉 Feedback

Fill out
the feedback form.

Basic JS "Hello World"

Basic JavaScript-rendered Hello World

📝 Your Notes

Generate DOM Nodes (Solution)

<body>
  <div id="root"></div>
  <script type="module">
    const rootElement = document.getElementById('root');
    const element = document.createElement('div');
    element.textContent = 'Hello World';
    element.className = 'container';
    rootElement.append(element);
  </script>
</body> 

Generate DOM Nodes (Extra)

<body>
  <script type="module">
    const rootElement = document.createElement('div');
    rootElement.setAttribute('id', 'root');
    document.body.append(rootElement);

    const element = document.createElement('div');
    element.textContent = 'Hello World';
    element.className = 'container';

    rootElement.append(element);
  </script>
</body>

Background

It doesn't take long to learn how to make "Hello World" appear on the page with
HTML:

<html>
  <body>
    <div>Hello World</div>
  </body>
</html>

The browser takes this HTML code and generates
the DOM (the Document Object Model)
out of it. The browser then exposes the DOM to JavaScript so you can interact
with it to add a layer of interactivity to your web-page.

<html>
  <body>
    <div>Hello World</div>
    <script type="module">
      // your JavaScript here
    </script>
  </body>
</html>

Years ago, people were generating HTML on the server and then adding JavaScript
on top of that generated HTML for interactivity. However, as requirements for
that interactivity became more challenging, this approach produced applications
that were difficult to maintain and had performance issues.

So modern JavaScript frameworks were created to address some of the challenges
by programmatically creating the DOM rather than defining it in hand-written
HTML.

Exercise

Production deploys:

It's important to have a basic understanding of how to generate and interact
with DOM nodes using JavaScript because it will help you understand how React
works under the hood a little better. So in this exercise we're actually not
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.

Extra Credit

1. 💯 generate the root node

Production deploy

Rather than having the root node in the HTML, see if you can create that one
using JavaScript as well.

🦉 Feedback

Fill out
the feedback form.

useEffect: persistent state

useEffect: persistent state

📝 Your Notes

Exercise

import * as React from 'react'

function Greeting({initialName = ''}) {
  // 🐨 initialize the state to the value from localStorage
  // 💰 window.localStorage.getItem('name') ?? initialName
  const [name, setName] = React.useState(
    window.localStorage.getItem('name') ?? initialName,
  )

  // 🐨 Here's where you'll use `React.useEffect`.
  // The callback should set the `name` in localStorage.
  // 💰 window.localStorage.setItem('name', name)
  React.useEffect(() => {
    window.localStorage.setItem('name', name)
  })

  function handleChange(event) {
    setName(event.target.value)
  }
  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting initialName="George" />
}

export default App

1. 💯 lazy state initialization

import * as React from 'react'

function Greeting({initialName = ''}) {
  const [name, setName] = React.useState(() => {
    console.log('getting initial value')
    return window.localStorage.getItem('name') ?? initialName
  })
  console.log('rendering')

  React.useEffect(() => {
    window.localStorage.setItem('name', name)
  })

  function handleChange(event) {
    setName(event.target.value)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting initialName="George" />
}

export default App
  • What we need to do is we need to somehow lazily read into this localStorage and say, "Hey, React, it's cool that you want to get this initial value. Can you instead maybe call a function that I am going to pass to you so that you can get that initial value and then not call it when you don't need the initial value?"
  • Creating a function is fast. Even if what the function does is computationally expensive. So you only pay the performance penalty when you call the function. So if you pass a function to useState, React will only call the function when it needs the initial value (which is when the component is initially rendered).
  • This is called "lazy initialization." It's a performance optimization. You shouldn't have to use it a whole lot, but it can be useful in some situations, so it's good to know that it's a feature that exists and you can use it when needed. I would say I use this only 2% of the time. It's not really a feature I use often.

2. 💯 effect dependencies

import * as React from 'react'

function Greeting({initialName = ''}) {
  console.log('rendering greeting')
  const [name, setName] = React.useState(() => {
    return window.localStorage.getItem('name') ?? initialName
  })

  React.useEffect(() => {
    window.localStorage.setItem('name', name)
    console.log('calling useEffect')
  }, [name])

  function handleChange(event) {
    setName(event.target.value)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  const [count, setCount] = React.useState(0)

  return (
    <>
      <button onClick={() => setCount(prev => prev + 1)}>{count}</button>
      {/* Greeting component could re-render because the parent re-rendered. */}
      <Greeting initialName="George" />
    </>
  )
}

export default App
  • To optimize away from this and only re-run our useEffect when the name does change, we added it to the dependency array here so that we can say, "Hey, React, I don't care about re-renders or anything like that. All I care about is, if the name changes, then that means the side effect that I'm trying to synchronize has fallen out of sync, and I need to make an update to the state of the world to match that change in my application."

3. 💯 custom hook

import * as React from 'react'

function useLocalStorageState(key, defaultValue = '') {
  const [state, setState] = React.useState(
    () => window.localStorage.getItem(key) ?? defaultValue,
  )

  React.useEffect(() => {
    window.localStorage.setItem(key, state)
  }, [key, state])

  return [state, setState]
}

function Greeting({initialName = ''}) {
  const [name, setName] = useLocalStorageState('name', initialName)

  function handleChange(event) {
    setName(event.target.value)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting initialName="George" />
}

export default App
  • Don't think about React, just think about how you take some pieces of code and abstract them, making them available for people to use anywhere in their codebase. The way that we do this is we make a function.
  • We didn't have to do any fancy React stuff at all. Let's just take this code; we'll put it in this function and pass some arguments, return some values, and then poof. It's all working. That's because the design of hooks was designed specifically so that you can make reusable code work exactly the same way as you do with regular JavaScript functions.

4. 💯 flexible localStorage hook (important)

import * as React from 'react'

function useLocalStorageState(
  key,
  defaultValue = '',
  // * What if the user of this hook didn't want to use JSON.stringify and JSON.parse,
  // * and they wanted to serialize and deserialize this a different way themselves?
  // * That's actually pretty straightforward to accomplish with some options here.
  // * If they don't want to provide options, then we'll default that to an empty object.
  {serialize = JSON.stringify, deserialize = JSON.parse} = {},
) {
  const [state, setState] = React.useState(() => {
    const valueInLocalstorage = window.localStorage.getItem(key)
    if (valueInLocalstorage) {
      // * We don't want it to be in quotes as it is here.
      // * I'm going to go ahead and clear that.
      // * We're going to fix that problem here by doing a JSON.parse.
      return deserialize(valueInLocalstorage)
    }

    // * The next thing that I want to add here is what if the default value
    // * that they want to provide to me is computationally expensive for them to create?
    // * If it's computationally expensive, then I don't want to have them have to pass that every single time.
    // * We're going to do basically the same thing that useState does,
    // * and that is make the default value optionally a function.
    // * We can say, "If the type of default value is a function, then we'll call default value.
    // * Otherwise, we'll just return the default value."
    return typeof defaultValue === 'function' ? defaultValue() : defaultValue
  })

  // * What we need to do is we want to remove the old value
  // * from the previous key and set the new one.
  // * To keep track of that previous value, what I'm going to do is have a prev key ref.
  // * That'll be React.useRef. We'll pass as the initial value the key.
  // * What this is going to do is this gives me an object that I can mutate
  // * without triggering rerenders.
  // * That differs from useState, because if I wanted to change this value,
  // * then I have to call setState to make a change to that value, and that'll trigger a rerender.
  // * For this particular thing, I don't want to trigger a rerender if I want to change that previous key.
  // * What we're going to do now is I'm going to get my previous key.
  // * That's going to be the previouskeyref.current. On the first render,
  // * that previouskeyref.current is going to be whatever that key was.
  // * Then I can say if the previous key is not equal to the current key,
  // * that may or may not have triggered this useEffect to get recalled.
  // * If those things have changed, then I'm going to say,
  // * "Hey, window, localStorage, I want to remove the previous item key."
  // * Let's get rid of the old one, I'm going to change it to a new one.
  // * Now we can say previous key ref.current = the new key.
  const prevKeyRef = React.useRef(key)

  React.useEffect(() => {
    const prevKey = prevKeyRef.current
    if (prevKey !== key) {
      window.localStorage.removeItem(prevKey)
    }
    prevKeyRef.current = key

    // * If that value is a number, then that value is going to get coerced into a string
    // * when we set that item into localStorage. Then when we try to get it out,
    // * it's going to be a string as well. We need to parse this somehow out and serialize it,
    // * stringify it maybe using JSON.stringify.
    window.localStorage.setItem(key, serialize(state))
  }, [key, state, serialize])

  return [state, setState]
}

function Greeting({initialName = ''}) {
  const [name, setName] = useLocalStorageState('name', initialName)

  function handleChange(event) {
    setName(event.target.value)
  }

  return (
    <div>
      <form>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

function App() {
  return <Greeting initialName="George" />
}

export default App

Background

React.useEffect is a built-in hook that allows you to run some custom code
after React renders (and re-renders) your component to the DOM. It accepts a
callback function which React will call after the DOM has been updated:

React.useEffect(() => {
  // your side-effect code here.
  // this is where you can make HTTP requests or interact with browser APIs.
})

Feel free to take a look at src/examples/hook-flow.png if you're interested in
the timing of when your functions are run. This will make more sense after
finishing the exercises/extra credit/instruction.
hook-flow

Exercise

Production deploys:

In this exercise, we're going to enhance our <Greeting /> component to get its
initial state value from localStorage (if available) and keep localStorage
updated as the name is updated.

Extra Credit

1. 💯 lazy state initialization

Production deploy

Right now, every time our component function is run, our function reads from
localStorage. This is problematic because it could be a performance bottleneck
(reading from localStorage can be slow). And what's more we only actually need
to know the value from localStorage the first time this component is rendered!
So the additional reads are wasted effort.

To avoid this problem, React's useState hook allows you to pass a function
instead of the actual value, and then it will only call that function to get the
state value when the component is rendered the first time. So you can go from
this: React.useState(someExpensiveComputation()) To this:
React.useState(() => someExpensiveComputation())

And the someExpensiveComputation function will only be called when it's
needed!

Make the React.useState call use lazy initialization to avoid a performance
bottleneck of reading into localStorage on every render.

Learn more about
lazy state initialization

2. 💯 effect dependencies

Production deploy

The callback we're passing to React.useEffect is called after every render
of our component (including re-renders). This is exactly what we want because we
want to make sure that the name is saved into localStorage whenever it
changes, but there are various reasons a component can be re-rendered (for
example, when a parent component in the application tree gets re-rendered).

Really, we only want localStorage to get updated when the name state
actually changes. It doesn't need to re-run any other time. Luckily for us,
React.useEffect allows you to pass a second argument called the "dependency
array" which signals to React that your effect callback function should be
called when (and only when) those dependencies change. So we can use this to
avoid doing unnecessary work!

Add a dependencies array for React.useEffect to avoid the callback being
called too frequently.

3. 💯 custom hook

Production deploy

The best part of hooks is that if you find a bit of logic inside your component
function that you think would be useful elsewhere, you can put that in another
function and call it from the components that need it (just like regular
JavaScript). These functions you create are called "custom hooks".

Create a custom hook called useLocalStorageState for reusability of all this
logic.

4. 💯 flexible localStorage hook

Production deploy

Take your custom useLocalStorageState hook and make it generic enough to
support any data type (remember, you have to serialize objects to strings... use
JSON.stringify and JSON.parse). Go wild with this!

Notes

If you'd like to learn more about when different hooks are called and the order
in which they're called, then open up src/examples/hook-flow.png and
src/examples/hook-flow.js. Play around with that a bit and hopefully that will
help solidify this for you. Note that understanding this isn't absolutely
necessary for you to understand hooks, but it will help you in some situations
so it's useful to understand.

PLEASE NOTE: there was a subtle change in the order of cleanup functions
getting called in React 17:
https://github.com/kentcdodds/react-hooks/issues/90

🦉 Feedback

Fill out
the feedback form.

Context Module Functions

Context Module Functions

📝 Your Notes

Exercise

// Context Module Functions
// http://localhost:3000/isolated/exercise/01.js

import * as React from 'react'
import {dequal} from 'dequal'

// ./context/user-context.js

import * as userClient from '../user-client'
import {useAuth} from '../auth-context'

const UserContext = React.createContext()
UserContext.displayName = 'UserContext'

function userReducer(state, action) {
  switch (action.type) {
    case 'start update': {
      return {
        ...state,
        user: {...state.user, ...action.updates},
        status: 'pending',
        storedUser: state.user,
      }
    }
    case 'finish update': {
      return {
        ...state,
        user: action.updatedUser,
        status: 'resolved',
        storedUser: null,
        error: null,
      }
    }
    case 'fail update': {
      return {
        ...state,
        status: 'rejected',
        error: action.error,
        user: state.storedUser,
        storedUser: null,
      }
    }
    case 'reset': {
      return {
        ...state,
        status: null,
        error: null,
      }
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function UserProvider({children}) {
  const {user} = useAuth()
  const [state, dispatch] = React.useReducer(userReducer, {
    status: null,
    error: null,
    storedUser: user,
    user,
  })
  const value = [state, dispatch]
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

function useUser() {
  const context = React.useContext(UserContext)
  if (context === undefined) {
    throw new Error(`useUser must be used within a UserProvider`)
  }
  return context
}

// 🐨 add a function here called `updateUser`
// Then go down to the `handleSubmit` from `UserSettings` and put that logic in
// this function. It should accept: dispatch, user, and updates
async function updateUser(dispatch, user, updates) {
  dispatch({type: 'start update', updates: updates})

  try {
    const updatedUser = await userClient.updateUser(user, updates)
    dispatch({type: 'finish update', updatedUser})
    return updatedUser
  } catch (error) {
    dispatch({type: 'fail update', error})
    throw error
  }
}

export {UserProvider, useUser, updateUser}

// src/screens/user-profile.js
// import {UserProvider, useUser, updateUser} from './context/user-context'
function UserSettings() {
  const [{user, status, error}, userDispatch] = useUser()

  const isPending = status === 'pending'
  const isRejected = status === 'rejected'

  const [formState, setFormState] = React.useState(user)

  const isChanged = !dequal(user, formState)

  function handleChange(e) {
    setFormState({...formState, [e.target.name]: e.target.value})
  }

  function handleSubmit(event) {
    event.preventDefault()
    updateUser(userDispatch, user, formState)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div style={{marginBottom: 12}}>
        <label style={{display: 'block'}} htmlFor="username">
          Username
        </label>
        <input
          id="username"
          name="username"
          disabled
          readOnly
          value={formState.username}
          style={{width: '100%'}}
        />
      </div>
      <div style={{marginBottom: 12}}>
        <label style={{display: 'block'}} htmlFor="tagline">
          Tagline
        </label>
        <input
          id="tagline"
          name="tagline"
          value={formState.tagline}
          onChange={handleChange}
          style={{width: '100%'}}
        />
      </div>
      <div style={{marginBottom: 12}}>
        <label style={{display: 'block'}} htmlFor="bio">
          Biography
        </label>
        <textarea
          id="bio"
          name="bio"
          value={formState.bio}
          onChange={handleChange}
          style={{width: '100%'}}
        />
      </div>
      <div>
        <button
          type="button"
          onClick={() => {
            setFormState(user)
            userDispatch({type: 'reset'})
          }}
          disabled={!isChanged || isPending}
        >
          Reset
        </button>
        <button
          type="submit"
          disabled={(!isChanged && !isRejected) || isPending}
        >
          {isPending
            ? '...'
            : isRejected
            ? '✖ Try again'
            : isChanged
            ? 'Submit'
            : '✔'}
        </button>
        {isRejected ? <pre style={{color: 'red'}}>{error.message}</pre> : null}
      </div>
    </form>
  )
}

function UserDataDisplay() {
  const [{user}] = useUser()
  return <pre>{JSON.stringify(user, null, 2)}</pre>
}

function App() {
  return (
    <div
      style={{
        minHeight: 350,
        width: 300,
        backgroundColor: '#ddd',
        borderRadius: 4,
        padding: 10,
      }}
    >
      <UserProvider>
        <UserSettings />
        <UserDataDisplay />
      </UserProvider>
    </div>
  )
}

export default App

Background

One liner: The Context Module Functions Pattern allows you to encapsulate a
complex set of state changes into a utility function which can be tree-shaken
and lazily loaded.

Let's take a look at an example of a simple context and a reducer combo:

// src/context/counter.js
const CounterContext = React.createContext()

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step
      switch (action.type) {
        case 'increment': {
          return {...state, count: state.count + change}
        }
        case 'decrement': {
          return {...state, count: state.count - change}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    },
    {count: initialCount},
  )

  const value = [state, dispatch]
  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  const context = React.useContext(CounterContext)
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`)
  }
  return context
}

export {CounterProvider, useCounter}
// src/screens/counter.js
import {useCounter} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  const increment = () => dispatch({type: 'increment'})
  const decrement = () => dispatch({type: 'decrement'})
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  )
}
// src/index.js
import {CounterProvider} from 'context/counter'

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  )
}

You can pull this example up here:
http://localhost:3000/isolated/examples/counter-before.js

I want to focus in on the user of our reducer (the Counter component). Notice
that they have to create their own increment and decrement functions which
call dispatch. I don't think that's a super great API. It becomes even more of
an annoyance when you have a sequence of dispatch functions that need to be
called (like you'll see in our exercise).

The first inclination is to create "helper" functions and include them in the
context. Let's do that. You'll notice that we have to put it in
React.useCallback so we can list our "helper" functions in dependency lists):

const increment = React.useCallback(
  () => dispatch({type: 'increment'}),
  [dispatch],
)
const decrement = React.useCallback(
  () => dispatch({type: 'decrement'}),
  [dispatch],
)
const value = {state, increment, decrement}
return <CounterContext.Provider value={value} {...props} />

// now users can consume it like this:

const {state, increment, decrement} = useCounter()

This isn't a bad solution necessarily. But
as my friend Dan says:

Helper methods are object junk that we need to recreate and compare for no
purpose other than superficially nicer looking syntax.

What Dan recommends (and what Facebook does) is pass dispatch as we had
originally. And to solve the annoyance we were trying to solve in the first
place, they use importable "helpers" that accept dispatch. Let's take a look
at how that would look:

// src/context/counter.js
const CounterContext = React.createContext()

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step
      switch (action.type) {
        case 'increment': {
          return {...state, count: state.count + change}
        }
        case 'decrement': {
          return {...state, count: state.count - change}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    },
    {count: initialCount},
  )

  const value = [state, dispatch]

  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  const context = React.useContext(CounterContext)
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`)
  }
  return context
}

const increment = dispatch => dispatch({type: 'increment'})
const decrement = dispatch => dispatch({type: 'decrement'})

export {CounterProvider, useCounter, increment, decrement}
// src/screens/counter.js
import {useCounter, increment, decrement} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={() => decrement(dispatch)}>-</button>
      <button onClick={() => increment(dispatch)}>+</button>
    </div>
  )
}

This may look like overkill, and it is. However, in some situations this
pattern can not only help you reduce duplication, but it also
helps improve performance
and helps you avoid mistakes in dependency lists.

I wouldn't recommend this all the time, but sometimes it can be a help!

📜 If you need to review the context API, here are the docs:

🦉 Tip: You may notice that the context provider/consumers in React DevTools
just display as Context.Provider and Context.Consumer. That doesn't do a
good job differentiating itself from other contexts that may be in your app.
Luckily, you can set the context displayName and it'll display that name for
the Provider and Consumer. Hopefully in the future this will happen
automatically (learn more).

const MyContext = React.createContext()
MyContext.displayName = 'MyContext'

Exercise

Production deploys:

👨‍💼 We have a user settings page where we render a form for the user's
information. We'll be storing the user's information in context and we'll follow
some patterns for exposing ways to keep that context updated as well as
interacting with the backend.

💰 In this exercise, if you enter the text "fail" in the tagline or biography
input, then the "backend" will reject the promise so you can test the error
case.

Right now the UserSettings form is calling userDispatch directly. Your job
is to move that to a module-level "helper" function that accepts dispatch as
well as the rest of the information that's needed to execute the sequence of
dispatches.

🦉 To keep things simple we're leaving everything in one file, but normally
you'll put the context in a separate module.

🦉 Feedback

Fill out
the feedback form.

useDebugValue: useMedia

useDebugValue: useMedia

📝 Your Notes

Exercise

import * as React from 'react'

const formatDebugValue = ({query, state}) => `\`${query}\` => ${state}`

function useMedia(query, initialState = false) {
  const [state, setState] = React.useState(initialState)
  // 🐨 call React.useDebugValue here.
  // 💰 here's the formatted label I use: `\`${query}\` => ${state}`
  React.useDebugValue({query, state}, formatDebugValue)
  /*
  Media: "`(min-width: 1000px)` => false"
  Media: "`(max-width: 999px) and (min-width: 700px)` => true"
  Media: "`(max-width: 699px)` => false"
   */

  React.useEffect(() => {
    let mounted = true
    const mql = window.matchMedia(query)
    function onChange() {
      if (!mounted) {
        return
      }
      setState(Boolean(mql.matches))
    }

    mql.addListener(onChange)
    setState(mql.matches)

    return () => {
      mounted = false
      mql.removeListener(onChange)
    }
  }, [query])

  return state
}

function Box() {
  const isBig = useMedia('(min-width: 1000px)')
  const isMedium = useMedia('(max-width: 999px) and (min-width: 700px)')
  const isSmall = useMedia('(max-width: 699px)')
  const color = isBig ? 'green' : isMedium ? 'yellow' : isSmall ? 'red' : null

  return <div style={{width: 200, height: 200, backgroundColor: color}} />
}

function App() {
  return <Box />
}

export default App
  • Use useDebugValue hook inside custom hook.
  • The only time you would ever actually use format function is if calculating that DebugValue is somehow expensive, and you want to optimize that away.

Background

The React DevTools browser extension
is a must-have for any React developer. When you start writing custom hooks, it
can be useful to give them a special label. This is especially useful to
differentiate different usages of the same hook in a given component.

This is where useDebugValue comes in. You use it in a custom hook, and you
call it like so:

function useCount({initialCount = 0, step = 1} = {}) {
  React.useDebugValue({initialCount, step})
  const [count, setCount] = React.useState(initialCount)
  const increment = () => setCount(c => c + step)
  return [count, increment]
}

So now when people use the useCount hook, they'll see the initialCount and
step values for that particular hook.

Exercise

Production deploys:

In this exercise, we have a custom useMedia hook which uses
window.matchMedia to determine whether the user-agent satisfies a given media
query. In our Box component, we're using it three times to determine whether
the screen is big, medium, or small and we change the color of the box based on
that.

Now, take a look at the png files associated with this exercise. You'll notice
that the before doesn't give any useful information for you to know which hook
record references which hook. In the after version, you'll see a really nice
label associated with each hook which makes it obvious which is which.

If you don't have the browser extension installed, install it now and open the
React tab in the DevTools. Select the <Box /> component in the React tree.
Your job is to use useDebugValue to provide a nice label.

Note: your hooks may look a tiny bit different from the screenshots thanks to
the fact that we're using
stop-runaway-react-effects.
Just focus on the label. That should be the same.

Extra Credit

1. 💯 use the format function

Production deploy

useDebugValue also takes a second argument which is an optional formatter
function, allowing you to do stuff like this if you like:

const formatCountDebugValue = ({initialCount, step}) =>
  `init: ${initialCount}; step: ${step}`

function useCount({initialCount = 0, step = 1} = {}) {
  React.useDebugValue({initialCount, step}, formatCountDebugValue)
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + step)
  return [count, increment]
}

This is only really useful for situations where computing the debug value is
computationally expensive (and therefore you only want it calculated when the
DevTools are open and not when your users are using the app). In our case this
is not necessary, however, go ahead and give it a try anyway.

🦉 Feedback

Fill out
the feedback form.

State Reducer

State Reducer

📝 Your Notes

Exercise

// State Reducer
// http://localhost:3000/isolated/exercise/05.js

import * as React from 'react'
import {Switch} from '../switch'

const toggleActionTypes = {
  toggle: 'TOGGLE',
  reset: 'RESET',
}

const callAll =
  (...fns) =>
  (...args) =>
    fns.forEach(fn => fn?.(...args))

function toggleReducer(state, {type, initialState}) {
  switch (type) {
    case toggleActionTypes.toggle: {
      return {on: !state.on}
    }
    case toggleActionTypes.reset: {
      return initialState
    }
    default: {
      throw new Error(`Unsupported type: ${type}`)
    }
  }
}

// 🐨 add a new option called `reducer` that defaults to `toggleReducer`
function useToggle({initialOn = false, reducer = toggleReducer} = {}) {
  const {current: initialState} = React.useRef({on: initialOn})
  // 🐨 instead of passing `toggleReducer` here, pass the `reducer` that's
  // provided as an option
  // ... and that's it! Don't forget to check the 💯 extra credit!
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const {on} = state

  const toggle = () => dispatch({type: toggleActionTypes.toggle})
  const reset = () => dispatch({type: toggleActionTypes.reset, initialState})

  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  function getResetterProps({onClick, ...props} = {}) {
    return {
      onClick: callAll(onClick, reset),
      ...props,
    }
  }

  return {
    on,
    reset,
    toggle,
    getTogglerProps,
    getResetterProps,
  }
}

function App() {
  const [timesClicked, setTimesClicked] = React.useState(0)
  const clickedTooMuch = timesClicked >= 4

  function toggleStateReducer(state, action) {
    switch (action.type) {
      case toggleActionTypes.toggle: {
        if (clickedTooMuch) {
          return {on: state.on}
        }
        return {on: !state.on}
      }
      case toggleActionTypes.reset: {
        return {on: false}
      }
      default: {
        throw new Error(`Unsupported type: ${action.type}`)
      }
    }
  }

  const {on, getTogglerProps, getResetterProps} = useToggle({
    reducer: toggleStateReducer,
  })

  return (
    <div>
      <Switch
        {...getTogglerProps({
          disabled: clickedTooMuch,
          on: on,
          onClick: () => setTimesClicked(count => count + 1),
        })}
      />
      {clickedTooMuch ? (
        <div data-testid="notice">
          Whoa, you clicked too much!
          <br />
        </div>
      ) : timesClicked > 0 ? (
        <div data-testid="click-count">Click count: {timesClicked}</div>
      ) : null}
      <button {...getResetterProps({onClick: () => setTimesClicked(0)})}>
        Reset
      </button>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • What we need to do is to make it so that people can fall back on the built-in reducer, but they can provide their own reducer if they want to manage how state updates happen for our custom hook.
  • This allows users of our custom hook or our custom component to specify their own reducer, which will accept the state and the action and return the state that they want to have based on what action was dispatched.

###1. 💯 default state reducer

// State Reducer
// http://localhost:3000/isolated/exercise/05.js

import * as React from 'react'
import {Switch} from '../switch'

const toggleActionTypes = {
  toggle: 'TOGGLE',
  reset: 'RESET',
}

const callAll =
  (...fns) =>
  (...args) =>
    fns.forEach(fn => fn?.(...args))

function toggleReducer(state, {type, initialState}) {
  switch (type) {
    case toggleActionTypes.toggle: {
      return {on: !state.on}
    }
    case toggleActionTypes.reset: {
      return initialState
    }
    default: {
      throw new Error(`Unsupported type: ${type}`)
    }
  }
}

// 🐨 add a new option called `reducer` that defaults to `toggleReducer`
function useToggle({initialOn = false, reducer = toggleReducer} = {}) {
  const {current: initialState} = React.useRef({on: initialOn})
  // 🐨 instead of passing `toggleReducer` here, pass the `reducer` that's
  // provided as an option
  // ... and that's it! Don't forget to check the 💯 extra credit!
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const {on} = state

  const toggle = () => dispatch({type: toggleActionTypes.toggle})
  const reset = () => dispatch({type: toggleActionTypes.reset, initialState})

  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  function getResetterProps({onClick, ...props} = {}) {
    return {
      onClick: callAll(onClick, reset),
      ...props,
    }
  }

  return {
    on,
    reset,
    toggle,
    getTogglerProps,
    getResetterProps,
  }
}

// export {toggleReducer, useToggle}

// import {toggleReducer, useToggle} from "./use-toggle"

function App() {
  const [timesClicked, setTimesClicked] = React.useState(0)
  const clickedTooMuch = timesClicked >= 4

  function toggleStateReducer(state, action) {
    if (action.type === toggleActionTypes.toggle && clickedTooMuch) {
      return {on: state.on}
    }
    return toggleReducer(state, action)
  }

  const {on, getTogglerProps, getResetterProps} = useToggle({
    reducer: toggleStateReducer,
  })

  return (
    <div>
      <Switch
        {...getTogglerProps({
          disabled: clickedTooMuch,
          on: on,
          onClick: () => setTimesClicked(count => count + 1),
        })}
      />
      {clickedTooMuch ? (
        <div data-testid="notice">
          Whoa, you clicked too much!
          <br />
        </div>
      ) : timesClicked > 0 ? (
        <div data-testid="click-count">Click count: {timesClicked}</div>
      ) : null}
      <button {...getResetterProps({onClick: () => setTimesClicked(0)})}>
        Reset
      </button>
    </div>
  )
}

export default App

/*
eslint
  no-unused-vars: "off",
*/
  • What would be cool is if we could say, "Hey, I want only to modify the way that the state's managed in certain scenarios, specifically when the actionType is toggle and when we've clicked too much. For everything else, you go ahead and do your normal stuff."
  • What we're going to do when we're building this useToggle is -- typically, this is going to be inside of another file -- we'll export the useToggle. Then we'll also export the toggleReducer. Then, I'm going to import the useToggle and toggleReducer from my useToggle module. I can use that inside of the toggleStateReducer.
  • All that we needed to do to support this is we export not only the hook but also the reducer function so that people can call it directly in their own reducer functions when they want to customize that state reducer.

Background

One liner: The State Reducer Pattern inverts control over the state
management of your hook and/or component to the developer using it so they can
control the state changes that happen when dispatching events.

During the life of a reusable component which is used in many different
contexts, feature requests are made over and over again to handle different
cases and cater to different scenarios.

We could definitely add props to our component and add logic in our reducer for
how to handle these different cases, but there's a never ending list of logical
customizations that people could want out of our custom hook and we don't want
to have to code for every one of those cases.

📜 Read more about this pattern in:
The State Reducer Pattern with React Hooks

Real World Projects that use this pattern:

Exercise

Production deploys:

In this exercise, we want to prevent the toggle from updating the toggle state
after it's been clicked 4 times in a row before resetting. We could easily add
that logic to our reducer, but instead we're going to apply a computer science
pattern called "Inversion of Control" where we effectively say: "Here you go!
You have complete control over how this thing works. It's now your
responsibility."

As an aside, before React Hooks were a thing, this was pretty tricky to
implement and resulted in pretty weird code, but with useReducer, this is WAY
better. I ❤️ hooks. 😍

Your job is to enable people to provide a custom reducer so they can have
complete control over how state updates happen in our <Toggle /> component.

Extra Credit

1. 💯 default state reducer

Production deploy

Our toggleReducer is pretty simple, so it's not a huge pain for people to
implement their own. However, in a more realistic scenario, people may struggle
with having to basically re-implement our entire reducer which could be pretty
complex. So see if you can provide a nice way for people to be able to use the
toggleReducer themselves if they so choose. Feel free to test this out by
changing the toggleStateReducer function inside the <App /> example to use
the default reducer instead of having to re-implement what to do when the action
type is 'reset':

function toggleStateReducer(state, action) {
  if (action.type === 'toggle' && timesClicked >= 4) {
    return {on: state.on}
  }
  return toggleReducer(state, action)
}

2. 💯 state reducer action types

Production deploy

Requiring people to know what action types are available and code them is just
asking for annoying typos (unless you're using TypeScript or Flow, which you
really should consider). See if you can figure out a good way to help people
avoid typos in those strings by perhaps putting all possible action types on an
object somewhere and referencing them instead of hard coding them.

🦉 Feedback

Fill out
the feedback form.

useEffect: HTTP requests

useEffect: HTTP requests

📝 Your Notes

Exercise

import * as React from 'react'
// 🐨 you'll want the following additional things from '../pokemon':
// fetchPokemon: the function we call to get the pokemon info
// PokemonInfoFallback: the thing we show while we're loading the pokemon info
// PokemonDataView: the stuff we use to display the pokemon info
import {
  PokemonForm,
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../pokemon'

function PokemonInfo({pokemonName}) {
  // 🐨 Have state for the pokemon (null)
  const [pokemon, setPokemon] = React.useState(null)

  // 🐨 use React.useEffect where the callback should be called whenever the
  // pokemon name changes.
  // 💰 DON'T FORGET THE DEPENDENCIES ARRAY!
  // 💰 if the pokemonName is falsy (an empty string) then don't bother making the request (exit early).
  // 🐨 before calling `fetchPokemon`, clear the current pokemon state by setting it to null.
  // (This is to enable the loading state when switching between different pokemon.)
  // 💰 Use the `fetchPokemon` function to fetch a pokemon by its name:
  //   fetchPokemon('Pikachu').then(
  //     pokemonData => {/* update all the state here */},
  //   )
  // 🐨 return the following things based on the `pokemon` state and `pokemonName` prop:
  //   1. no pokemonName: 'Submit a pokemon'
  //   2. pokemonName but no pokemon: <PokemonInfoFallback name={pokemonName} />
  //   3. pokemon: <PokemonDataView pokemon={pokemon} />
  React.useEffect(() => {
    if (!pokemonName) return
    setPokemon(null) // reset state to show loading state

    fetchPokemon(pokemonName).then(pokemonData => {
      setPokemon(pokemonData)
    })
  }, [pokemonName])

  if (!pokemonName) {
    return 'Submit a pokemon'
  } else if (!pokemon) {
    return <PokemonInfoFallback name={pokemonName} />
  } else {
    return <PokemonDataView pokemon={pokemon} />
  }
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        <PokemonInfo pokemonName={pokemonName} />
      </div>
    </div>
  )
}

export default App

1. 💯 handle errors

import * as React from 'react'
import {
  PokemonForm,
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../pokemon'

function PokemonInfo({pokemonName}) {
  const [pokemon, setPokemon] = React.useState(null)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    if (!pokemonName) return

    setPokemon(null) // reset state to show loading state
    setError(null) // reset error state

    fetchPokemon(pokemonName).then(
      pokemonData => setPokemon(pokemonData),
      error => setError(error),
    )
    // * // option 1: using .catch
    // * You'll handle an error in the fetchPokemon promise,
    // * but you'll also handle an error in the setPokemon(pokemon) call as well.

    // * option 2: using the second argument to .then
    // * You will catch an error that happens in fetchPokemon only.
  }, [pokemonName])

  if (error) {
    return (
      <div role="alert">
        There was an error:{' '}
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  }

  if (!pokemonName) {
    return 'Submit a pokemon'
  } else if (!pokemon) {
    return <PokemonInfoFallback name={pokemonName} />
  } else {
    return <PokemonDataView pokemon={pokemon} />
  }
}

2. 💯 use a status

import * as React from 'react'
import {
  PokemonForm,
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../pokemon'

function PokemonInfo({pokemonName}) {
  const [status, setStatus] = React.useState('idle')
  const [pokemon, setPokemon] = React.useState(null)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    if (!pokemonName) return

    setStatus('pending')

    fetchPokemon(pokemonName).then(
      pokemonData => {
        setPokemon(pokemonData)
        setStatus('resolved')
      },
      error => {
        setError(error)
        setStatus('rejected')
      },
    )
  }, [pokemonName])

  // * Render different UI based on that status variable
  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    return (
      <div role="alert">
        There was an error:{' '}
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }

  throw new Error('This should be impossible')
}

3. 💯 store the state in an object

import * as React from 'react'
import {
  PokemonForm,
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../pokemon'

function PokemonInfo({pokemonName}) {
  const [state, setState] = React.useState({
    status: 'idle',
    pokemon: null,
    error: null,
  })
  const {status, pokemon, error} = state

  React.useEffect(() => {
    if (!pokemonName) return

    setState({status: 'pending'})

    fetchPokemon(pokemonName).then(
      pokemon => {
        setState({status: 'resolved', pokemon})
      },
      error => {
        setState({status: 'rejected', error})
      },
    )
  }, [pokemonName])

  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    return (
      <div role="alert">
        There was an error:{' '}
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }

  throw new Error('This should be impossible')
}

4. 💯 create an ErrorBoundary component

import * as React from 'react'
import {
  PokemonForm,
  fetchPokemon,
  PokemonInfoFallback,
  PokemonDataView,
} from '../pokemon'

class ErrorBoundary extends React.Component {
  state = {error: null}

  static getDerivedStateFromError(error) {
    return {error}
  }

  render() {
    // console.log('ErrorBoundary', this.state.error) // ErrorBoundary null
    const {error} = this.state

    if (error) {
      return <this.props.FallbackComponent error={error} />
    }

    return this.props.children
  }
}

function ErrorFallback({error}) {
  return (
    <div role="alert">
      There was an error:{' '}
      <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
    </div>
  )
}

function PokemonInfo({pokemonName}) {
  const [state, setState] = React.useState({
    status: 'idle',
    pokemon: null,
    error: null,
  })
  const {status, pokemon, error} = state

  React.useEffect(() => {
    if (!pokemonName) return

    setState({status: 'pending'})

    fetchPokemon(pokemonName).then(
      pokemon => {
        setState({status: 'resolved', pokemon})
      },
      error => {
        setState({status: 'rejected', error})
      },
    )
  }, [pokemonName])

  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    // This will be handled by our error boundary
    throw error
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }

  throw new Error('This should be impossible')
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        <ErrorBoundary FallbackComponent={ErrorFallback}>
          <PokemonInfo pokemonName={pokemonName} />
        </ErrorBoundary>
      </div>
    </div>
  )
}

export default App
  • The error boundary, by default, will just render all the children, so it's just a regular wrapper, it doesn't really do anything, but if there is an error in there, then react will look for the closest error boundary or the closest component that implements the static method and it will pass us the error, which we can use to set our state.

5. 💯 re-mount the error boundary

<ErrorBoundary FallbackComponent={ErrorFallback} key={pokemonName}>

6. 💯 use react-error-boundary

  • Just replace my own error boundary component with component from react-error-boundary package

7. 💯 reset the error boundary

  • code link

  • The problem

    • If you watch really carefully, I don't know if the framerate's going to be fast enough for you. If you watch carefully on your own computer, you'll notice that we see Submit a Pokémon right here when I click on here. Watch. Boom! Did you see it? Just like a fraction of a second. It showed up Submit a Pokémon. If you didn't see it, then just trust me, it's there.

    • What's going on here is when we set this key, every single time we change the Pokémon name, it's going to completely unmount this ErrorBoundary, which in turn unmounts the Pokémon info, and then it's going to mount a new instance of each of these elements. That's how the key works.

    • We'll see that Submit a Pokémon show up for every single Pokémon that we put in here. Not only when the ErrorBoundary has an error, but also every time we change that Pokémon name. Because we're changing the key, therefore, unmounting and remounting both of these. When this gets mounted, we take a look at the Pokémon info component, we initialize our status to idle. When the status is idle, it's going to say Submit a Pokémon. Immediately, we have our useEffect run.

    • The core problem still exists where we're completely unmounting this Pokémon info component and remounting it every single time we change the Pokémon name.

    • I wanted to unmount and remount when we have an error because that's a totally different situation, but if we're just switching the props, I don't want that to unmount and remount every single time. That's a waste, and I don't want to deal with that.

    • We're going to fix this by using some of the features that react-error-boundary's ErrorBoundary has built-in, and that specifically is that our error fallback gets rendered with the props error and resetErrorBoundary.

    • With this, we can explicitly reset the state without having to unmount and remount this component. I'm going to get rid of that key, and we're going to use this reset error boundary and add a button where onClick calls reset error boundary, and in here, we'll just say, "Try again."

    • react-error-boundary API

8. 💯 use resetKeys

        <ErrorBoundary
          FallbackComponent={ErrorFallback}
          onReset={handleReset}
          resetKeys={[pokemonName]}
        >

Background

HTTP requests are another common side-effect that we need to do in applications.
This is no different from the side-effects we need to apply to a rendered DOM or
when interacting with browser APIs like localStorage. In all these cases, we do
that within a useEffect hook callback. This hook allows us to ensure that
whenever certain changes take place, we apply the side-effects based on those
changes.

One important thing to note about the useEffect hook is that you cannot return
anything other than the cleanup function. This has interesting implications with
regard to async/await syntax:

// this does not work, don't do this:
React.useEffect(async () => {
  const result = await doSomeAsyncThing()
  // do something with the result
})

The reason this doesn't work is because when you make a function async, it
automatically returns a promise (whether you're not returning anything at all,
or explicitly returning a function). This is due to the semantics of async/await
syntax. So if you want to use async/await, the best way to do that is like so:

React.useEffect(() => {
  async function effect() {
    const result = await doSomeAsyncThing()
    // do something with the result
  }
  effect()
})

This ensures that you don't return anything but a cleanup function.

🦉 I find that it's typically just easier to extract all the async code into a
utility function which I call and then use the promise-based .then method
instead of using async/await syntax:

React.useEffect(() => {
  doSomeAsyncThing().then(result => {
    // do something with the result
  })
})

But how you prefer to do this is totally up to you :)

Exercise

Production deploys:

In this exercise, we'll be doing data fetching directly in a useEffect hook
callback within our component.

Here we have a form where users can enter the name of a pokemon and fetch data
about that pokemon. Your job will be to create a component which makes that
fetch request. When the user submits a pokemon name, our PokemonInfo component
will get re-rendered with the pokemonName

Extra Credit

1. 💯 handle errors

Production deploy

Unfortunately, sometimes things go wrong and we need to handle errors when they
do so we can show the user useful information. Handle that error and render it
out like so:

<div role="alert">
  There was an error: <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
</div>

You can make an error happen by typing an incorrect pokemon name into the input.

One common question I get about this extra credit is how to handle promise
errors. There are two ways to do it in this extra credit:

// option 1: using .catch
fetchPokemon(pokemonName)
  .then(pokemon => setPokemon(pokemon))
  .catch(error => setError(error))

// option 2: using the second argument to .then
fetchPokemon(pokemonName).then(
  pokemon => setPokemon(pokemon),
  error => setError(error),
)

These are functionally equivalent for our purposes, but they are semantically
different in general.

Using .catch means that you'll handle an error in the fetchPokemon promise,
but you'll also handle an error in the setPokemon(pokemon) call as well.
This is due to the semantics of how promises work.

Using the second argument to .then means that you will catch an error that
happens in fetchPokemon only. In this case, I knew that calling setPokemon
would not throw an error (React handles errors and we have an API to catch those
which we'll use later), so I decided to go with the second argument option.

However, in this situation, it doesn't really make much of a difference. If you
want to go with the safe option, then opt for .catch.

2. 💯 use a status

Production deploy

Our logic for what to show the user when is kind of convoluted and requires that
we be really careful about which state we set and when.

We could make things much simpler by having some state to set the explicit
status of our component. Our component can be in the following "states":

  • idle: no request made yet
  • pending: request started
  • resolved: request successful
  • rejected: request failed

Try to use a status state by setting it to these string values rather than
relying on existing state or booleans.

Learn more about this concept here:
https://kentcdodds.com/blog/stop-using-isloading-booleans

💰 Warning: Make sure you call setPokemon before calling setStatus. We'll
address that more in the next extra credit.

3. 💯 store the state in an object

Production deploy

You'll notice that we're calling a bunch of state updaters in a row. This is
normally not a problem, but each call to our state updater can result in a
re-render of our component. React normally batches these calls so you only get a
single re-render, but it's unable to do this in an asynchronous callback (like
our promise success and error handlers).

So you might notice that if you do this:

setStatus('resolved')
setPokemon(pokemon)

You'll get an error indicating that you cannot read image of null. This is
because the setStatus call results in a re-render that happens before the
setPokemon happens.

but it's unable to do this in an asynchronous callback

This is no longer the case in React 18 as it supports automatic batching for asynchronous callback too.

Learn more about this concept here:
https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching

Still it is better to maintain closely related states as an object rather than maintaining them using individual useState hooks.

Learn more about this concept here:
https://kentcdodds.com/blog/should-i-usestate-or-usereducer#conclusion

In the future, you'll learn about how useReducer can solve this problem really
elegantly, but we can still accomplish this by storing our state as an object
that has all the properties of state we're managing.

See if you can figure out how to store all of your state in a single object with
a single React.useState call so I can update my state like this:

setState({status: 'resolved', pokemon})

4. 💯 create an ErrorBoundary component

Production deploy

We've already solved the problem for errors in our request, we're only handling
that one error. But there are a lot of different kinds of errors that can happen
in our applications.

No matter how hard you try, eventually your app code just isn’t going to behave
the way you expect it to and you’ll need to handle those exceptions. If an error
is thrown and unhandled, your application will be removed from the page, leaving
the user with a blank screen... Kind of awkward...

Luckily for us, there’s a simple way to handle errors in your application using
a special kind of component called an
Error Boundary. Unfortunately,
there is currently no way to create an Error Boundary component with a function
and you have to use a class component instead.

In this extra credit, read up on ErrorBoundary components, and try to create one
that handles this and any other error for the PokemonInfo component.

💰 to make your error boundary component handle errors from the PokemonInfo
component, instead of rendering the error within the PokemonInfo component,
you'll need to throw error right in the function so React can hand that to the
error boundary. So if (status === 'rejected') throw error.

5. 💯 re-mount the error boundary

Production deploy

You might notice that with the changes we've added, we now cannot recover from
an error. For example:

  1. Type an incorrect pokemon
  2. Notice the error
  3. Type a correct pokemon
  4. Notice it doesn't show that new pokemon's information

The reason this is happening is because the error that's stored in the
internal state of the ErrorBoundary component isn't getting reset, so it's not
rendering the children we're passing to it.

So what we need to do is reset the ErrorBoundary's error state to null so it
will re-render. But how do we access the internal state of our ErrorBoundary
to reset it? Well, there are a few ways we could do this by modifying the
ErrorBoundary, but one thing you can do when you want to reset the state of
a component, is by providing it a key prop which can be used to unmount and
re-mount a component.

The key you can use? Try the pokemonName!

6. 💯 use react-error-boundary

Production deploy

As cool as our own ErrorBoundary is, I'd rather not have to maintain it in the
long-term. Luckily for us, there's an npm package we can use instead and it's
already installed into this project. It's called
react-error-boundary.

Go ahead and give that a look and swap out our own ErrorBoundary for the one
from react-error-boundary.

7. 💯 reset the error boundary

Production deploy

You may have noticed a problem with the way we're resetting the internal state
of the ErrorBoundary using the key. Unfortunately, we're not only
re-mounting the ErrorBoundary, we're also re-mounting the PokemonInfo which
results in a flash of the initial "Submit a pokemon" state whenever we change
our pokemon.

So let's backtrack on that and instead we'll use react-error-boundary's
resetErrorBoundary function (which will be passed to our ErrorFallback
component) to reset the state of the ErrorBoundary when the user clicks a "try
again" button.

💰 feel free to open up the finished version by clicking the link in the app
so you can get an idea of how this is supposed to work.

Once you have this button wired up, we need to react to this reset of the
ErrorBoundary's state by resetting our own state so we don't wind up
triggering the error again. To do this we can use the onReset prop of the
ErrorBoundary. In that function we can simply setPokemonName to an empty
string.

8. 💯 use resetKeys

Production deploy

Unfortunately now the user can't simply select a new pokemon and continue with
their day. They have to first click "Try again" and then select their new
pokemon. I think it would be cooler if they can just submit a new pokemonName
and the ErrorBoundary would reset itself automatically.

Luckily for us react-error-boundary supports this with the resetKeys prop.
You pass an array of values to resetKeys and if the ErrorBoundary is in an
error state and any of those values change, it will reset the error boundary.

💰 Your resetKeys prop should be: [pokemonName]

🦉 Feedback

Fill out
the feedback form.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.