Skip to content

zacjones93/react-hooks-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

React Hooks

Table of Contents

  1. React Hooks
    1. General
    2. useState: greeting
      1. useState
      2. insitial Props
    3. useEffect: persistent state
    4. Hooks Flow
    5. Lifting state
      1. Solution: Lift State
      2. ECS01: Colocate State
    6. useState: tic tac toe
      1. Solution: Managaged and Derieved State
      2. ECS01: Preserve State in localStorage
      3. ECS02: useLocalStorageState Custom Hook
      4. ECS03: Add Game History Feature
    7. Class Refactor
    8. useRef and useEffect: DOM interaction
      1. Solution
    9. useEffect: HTTP requests
      1. Solution Fetch Data
      2. ECS01 handle errors
      3. ECS02 use a status
      4. ECS03 store the state in an object
      5. ECS04 create an ErrorBoundary component
      6. ECS05 re-mount the error boundary
      7. ECS06 use react-error-boundary
      8. ECS07 reset the error boundary
      9. ECS08 use resetKeys

General

https://github.com/kentcdodds/react-hooks

useState: greeting

React uses ’hooks’ to build in interactivity into an application

Common hooks are: React.useState React.useEffect React.useContext React.useRef React.useReducer

useState

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.

-> setVariable

if you were to declare a variable in a React component, you can’t update it because the function that gets called only get’s called once.

function Greeting(props) {
  let name = ''

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

  return (...)

}

name will only ever get set once and we won’t be able to update it.

useState will help us out here! you can setName which is a function that will let you update state that is returned to use from useState

const [name, setName] = useState('')

Now you just set the name and you are updating state in React!

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

insitial Props

useState accepts an arguement that it will initialize state with.

You can use React props to initialize that state and pass it in!

You can set the value of the input to name so that it’s a controlled React input

function Greeting({initialName = ''}) {
  const [name, setName] = useState(name)

    // ...
  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="Jill" />
}

useEffect: persistent state

side effects are handled through the useEffect hook.

an example is setting state values in localStorage so State can persist through a refresh

https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

Any time the component renders, useEffect will get called

function Greeting({initialName = ''}) {
  // 🐨 initialize the state to the value from localStorage

  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)
  }, [name])

//...
}
  1. ECS01: lazy state initialization

    React will initialize state every time in a useState hook if you simply pass it a value.

    It will re-render every time a value changes.

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

    const [name, setName] = React.useState(
      () => window.localStorage.getItem('name') || initialName,
    )
    

    This optimization isn’t often needed. Accessing localStorage on initialization is one of the few times that you’d need this.

  2. ECS02: effect dependencies

    There are various reasons a component can re-render. Sometimes you don’t want an effect to be triggered on a re-render. This is often tied to a specific piece of state that we want to watch for updates.

    useEffect gives us an optional array argument that we can use to specifify when we want the effect to run.

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

    The above code will only run when the name variable changes. Other state changes or renders will not run the effect.

    Passing an empty array will have the effect only run on initializaion of the Component.

    React does a shallow comparison of the item passed into the array.

  3. ECS03: custom hook

    const useLocalStorageState = ({key, initialState = ''}) => {
      const [state, setState] = React.useState(
        () => window.localStorage.getItem(key) || initialState,
      )
    
      React.useEffect(() => {
        window.localStorage.setItem(key, state)
      }, [state])
    
      return [state, setState]
    }
    

    The variables and functions you export from a hook can be renamed in the resulting call to the hook so the hook itself can be generic.

    const [name, setName] = useLocalStorageState(initialName)
    

    There is a convention to prepend a function with use but doing so isn’t what makes it a custom hook. Using other hooks and adding custom functionality is what makes it a custom hook!

  4. ECS04: flexible localStorage hook

    The amount of logic that you include in your custom hook does not have to affect the complexity to use it.

    The example here is making the hook more flexible by stringifying the localstorage value and parsing it when it gets read.

    You can even give someone the option to roll their own serilization in a third argument.

    Another optimization is to allow the default State value to be a function like we’ve seen in lazy state initialization lesson.

    in the useState hook:

    return typeof initialState === 'function' ? initialState() : initialState
    

    if you want to keep track of a value without triggering a re-render, useRef is your friend. The case here is tracking if the key for localstorage changes to update it to the new one without scraping the current value.

    const prevKeyRef = React.useRef(key)
    
      React.useEffect(() => {
        const prevKey = prevKeyRef.current
        if(prevKey !== key) {
          window.localStorage.removeItem(prevKey)
        }
        prevKeyRef.current = key
        window.localStorage.setItem(key, serialize(state))
      }, [state, serialize, key])
    

Hooks Flow

Lifting state

Solution: Lift State

ECS01: Colocate State

useState: tic tac toe

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

Not all state has to be managed by React! You can declare variables in state that derive from the state that you want to manage

Don’t sync state; derive it

Solution: Managaged and Derieved State

const [squares, setSquares] = React.useState(Array(9).fill(null))

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

nextValue, winner, and status don’t need to be managed through react because they are determined by the values within squares

This will keep all values in sync where if you tried to sync them through React you could run into subtle bugs where values don’t update automatically like you would expect

ECS01: Preserve State in localStorage

ECS02: useLocalStorageState Custom Hook

ECS03: Add Game History Feature

Adding complexity into an application can get messy real quick.

One great place to start is the static representation of what the data will look like on the screen when it’s there. Once you get that rendering correctly, you can work backwards to add the dynamic aspects of the feature.

For the history feature, starting with what moves looks like is the way:

const moves = history.map((stepSquares, step) => {
  const desc = step ? `Go to move #${step}` : 'Go to game start'
  const isCurrentStep = step === currentStep
  return (
    <li key={step}>
      <button disabled={isCurrentStep} onClick={() => setCurrentStep(step)}>
        {desc} {isCurrentStep ? '(current)' : null}
      </button>
    </li>
  )
})

There are two new variables here from before: history and currentStep

Set them to expected values

const history = [Array(9).fill(null)]
const currentStep = 0

Once that renders correctly, try more complex data

const history = [[], Array(9).fill(null), []]
const currentStep = 2

Now the UI renders correctly, we can implement this.

const [history, setHistory] = useLocalStorageState('tic-tac-toe:history', [
    Array(9).fill(null),
  ])
  const [currentStep, setCurrentStep] = useLocalStorageState(
    'tic-tac-toe:step',
    0,
  )

The rest of our logic expects 1 array that represents the board - that can be derived from history because the current board state is now a derivative to the whole game history

const currentSquares = history[currentStep]

🤔 You don’t need to force the functionality into the current implementation, build it on top and transfer. I made the mistake of trying to extend squares to include a history instead of building history and then seeing that the current squares could be derived.

The only updates to existing code is setting history and current step. and subsequently

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

    const newHistory = history.slice(0, currentStep + 1)
    const squares = [...currentSquares]

    squares[square] = nextValue
    setHistory([...newHistory, squares])
    setCurrentStep(newHistory.length)
  }

  function restart() {
    setHistory([Array(9).fill(null)])
    setCurrentStep(0)
  }

Class Refactor

useRef and useEffect: DOM interaction

You don’t normally have access to DOM nodes in the ReactDOM render method.

React’s method for giving you access to the actual DOM is through Refs.

We’ve seen refs before as they don’t trigger renders and stay consistent between

Because you need the component to be mounted, you will do direct DOM munipulation in a useEffect

Solution

function Tilt({children}) {
    console.log(tiltRef)

    // ...
}

tiltRef will be undefined because the component isn’t fully mounted when console.log is run.

Need a useEffect to ensure the Component is mounted.

useEffect’s return will be the clean up function that you might need to run when the Component is removed from the DOM. In our case, we need to destroy vanillaTilt

The Ref current property is mutable

Do you need to synchronize the state of the world with the state of your application? -> Answer is what you put in your useEffect dependency array

useEffect: HTTP requests

Solution Fetch Data

Write the UI first! then add the interactivity.

return !pokemonName ? (
    'Submit a Pokemon'
  ) : pokemon ? (
    <PokemonDataView pokemon={pokemon} />
  ) : (
    <PokemonInfoFallback name={pokemonName} />
  )

HTTP requests are side effects of a component so they go in a useEffect

React.useEffect(() => {
    if (!pokemonName) return

    setLoadState('pending')
    setPokemon(null)
    fetchPokemon(pokemonName).then(pokemonData => {
      setPokemon(pokemonData)
      setLoadState('resolved')
    })
  }, [pokemonName])

ECS01 handle errors

If you pass in a value that doesn’t exist you’ll make your user think they are stuck in a loading state.

Alot of handling errors is UX.

Start with static data again!

fetchPokemon(pokemonName)
   .then(pokemonData => setPokemon(pokemonData))
   .catch(err => setError(err))
)

You can ’catch’ an error like this or pass the function in as a second argument to then:

fetchPokemon(pokemonName).then(
  pokemonData => setPokemon(pokemonData),
  err => setError(err),
)

ECS02 use a status

Using Booleans to define what JSX gets rendered starts getting cumbersome. If you set an error boolean, you have to remember to unset it when a user performs another action or else that error UI will persist.

Using a seperate ’status’ variable we can define what gets shown. For HTTP requests there are typically 4 states: idle, pending, resolved, or rejected

const STATUS = {
  idle: 'idle',
  pending: 'pending',
  resolved: 'resolved',
  rejected: 'rejected',
}

if (status === STATUS.idle) {
    return 'Submit a Pokemon'
} else if (status === STATUS.rejected) {
    return (
      <div role="alert">
        There was an error:{' '}
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  } else if (status === STATUS.pending) {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === STATUS.resolved) {
    return <PokemonDataView pokemon={pokemon} />
  }

ECS03 store the state in an object

When you have a handful of state objects that depend on eachother (status and pokemon), managing them in separate useState calls can be cumbersome and introduce subtle errors.

One of these errors is that you have to set state in the right order or you will try to render data before it’s available. In this case status and pokemon

You can fix this issue by putting your seperate state items into one object that a useState call manages.

const [state, setState] = React.useState({
  status: 'idle',
  pokemon: null,
  error: null,
})
const {status, pokemon, error} = state

then you just have to update your setState calls

React.useEffect(() => {
  if (!pokemonName) {
    return
  }
  setState({status: 'pending'})
  fetchPokemon(pokemonName).then(
    pokemon => {
      setState({status: 'resolved', pokemon})
    },
    error => {
      setState({status: 'rejected', error})
    },
  )
}, [pokemonName])

Something to keep in mind, when you setState({status: 'pending'}) you remove the pokemon and error attributes in state. This isn’t a problem for us in this case but if you depended on those values elsewhere in your UI you could run into issues.

ECS04 create an ErrorBoundary component

ErrorBoundary’s are one of the only things you’ll have to use that still implement classes.

Runtime errors do happen sometime, so a useful error is much nicer than the white screen of death.

Every class component needs to have a render method on it.

class ErrorBoundary extends React.Component {
    state = {error: null}

    static getDerivedStateFromError(error) {
     return {error}
    }

    render() {
      if (this.state.error) {
        // You can render any custom fallback UI
        return (
          <div role="alert">
            <h1>Something went wrong.</h1>
            <details style={{whiteSpace: 'pre-wrap'}}>
              {this.state.error && this.state.error.toString()}
              <br />
            </details>
          </div>
        )
      }

      return this.props.children
    }
}

getDerivedStateFromError will set the error property in state because you returned it so you don’t need to do any setState calls. (Maybe you can’t when React is v16.13.1 - I was running into errors trying to setState).

To make the component much more flexible, you can pass a prop to it called FallbackComponent so that the user can define what UI they want to use for showing their error

class ErrorBoundary extends React.Component {
    state = {error: null}

    static getDerivedStateFromError(error) {
     return {error}
    }

    render() {
      if (this.state.error) {
        // You can render any custom fallback UI
        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>
  )
}

//...
  <ErrorBoundary FallbackComponent={ErrorFallback}>
    <PokemonInfo pokemonName={pokemonName} />
  </ErrorBoundary>

ECS05 re-mount the error boundary

When an error boundary is triggered - that state is stuck so the app will no longer work.

All you need to do is add a key that is unique (which happens to be pokemonName) which will force React to rerender the component and reset the state thus clearing the error so we can search for pokemon again.

return (
  <div className="pokemon-info-app">
    <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
    <hr />
    <div className="pokemon-info">
      <ErrorBoundary key={pokemonName} FallbackComponent={ErrorFallback}>
        <PokemonInfo pokemonName={pokemonName} />
      </ErrorBoundary>
    </div>
  </div>
)

ECS06 use react-error-boundary

import {ErrorBoundary} from 'react-error-boundary'

react-error-boundary will let you handle your error boundaries without you having to build out a class component, YAY!

ECS07 reset the error boundary

Switching the props currently re-renders the whole component every single time since we set the key.

The library we imported has quite a few features. one of those is a resetErrorBoundary function that will reset the state and a onReset prop that we can use to clear our component state with the ErrorBoundary reset.

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">
        <ErrorBoundary FallbackComponent={ErrorFallback} onReset={handleReset}>
          <PokemonInfo pokemonName={pokemonName} />
        </ErrorBoundary>
      </div>
    </div>
  )
}

function ErrorFallback({resetErrorBoundary, error}) {
  return (
    <div role="alert">
      <h1>Something went wrong.</h1>
      <button
        onClick={() => {
          resetErrorBoundary()
        }}
      >
        Try Again
      </button>
      <details style={{whiteSpace: 'pre-wrap'}}>
        {error && error.toString()}
        <br />
      </details>
    </div>
  )
}

ECS08 use resetKeys

The api gets nicer!

resetKeys is a prop you can pass to the ErrorBoundary that will reset the error state similar to our keys implimentation we had earlier.

Now the user has the flexibility to use the Try Again button or try another pokemon name.

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">
        <ErrorBoundary
          FallbackComponent={ErrorFallback}
          onReset={handleReset}
          resetKeys={[pokemonName]}
        >
          <PokemonInfo pokemonName={pokemonName} />
        </ErrorBoundary>
      </div>
    </div>
  )
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published