Lesson 08: Built in React APIs - strvcom/frontend-academy-2022 GitHub Wiki

Speaker: Yulia Butyrskaya

Resources

  • Pull Request - Implementation of Login Form. (Will later on be replaced with a form library)
    • Pull Request Draft - Advanced: Form State Management, Step 2 - useLoginForm. (Won't be merged to the main branch)
    • Pull Request Draft - Advanced: Form State Management, Step 3 - useForm. (Won't be merged to the main branch)
  • Recording (Gdrive)
  • Slides (Google Slides)
  • Slides (PDF)

React before Hooks

Classes

  • Are required to access state and lifecycle methods
  • Are confusing, comes with an annoying boilerplate, and is hard to minify and optimize
  • The logic split across different lyfecycle methods makes it hard to follow it through a component  

HOC & Render Props

  • Are the most popular patterns to share stateful logic because React doesn't have any official way to do that
  • It makes it difficult to follow data flow through the app and debug
  • HOC creates "Wrapping hell"
  • Painful refactoring

Hooks

  • Let you use state and other React features without writing a class
  • Made it possible to colocate and reuse stateful logic together
  • You can write your own  

Rules of hooks

Don'ts ❌

  • Don't call Hooks inside loops, conditions, or nested functions
  • Don't call Hooks from regular JavaScript functions

Dos ✅

  • Call Hooks from React function components
  • Call Hooks from custom Hooks

useState

  • Lets to add state to function components
  • Declares a 'state variable' that stays preserved between re-renders
const Counter = () => {
  const [count, setCount] = useState(0)
    
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click</button>
        <p>You clicked {count} times</p>
    </div>
  )
}
  • We can calculate a new state based on the previous one:
setCount((previousValue) => previousValue + 1)
  • We can reset state value using reserved key prop:
const Counter = () => {
  const [count, setCount] = useState(0)
    
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click</button>
        <p>You clicked {count} times</p>
    </div>
  )
}

const App = () => {
  const [key, setKey] = useState(0)

  return (
    <>
      <Counter key={key} />
      <button onClick={() => setKey(key + 1)}>Reset</button>
    </>
  )
}
  • If the initial value is a result of an expensive computation, we can pass a function, which will be executed only on the initial render:
const getInitialValue = () => {
  // expensive computation
  return 42
}

const [value, setValue] = useState(getInitialValue) // note: we're not calling this function there!

useEffect

  • Lets to perform side effects in function components
  • In other words it runs some additional code after (re-)render
  • By default, it runs both after the first render and after every update, but it is customizable:
  • No dependency array => runs on every (re-)render
  • Empty array [] => runs on the first render only
  • Array with values [value1, value2] => runs only when valueX has changed
const Greeting = ({ initialName = '' }) => {
  console.log('%c    [Child] render start', 'color: LightCoral')

  const getInitialValue = () => {
    console.log('%c    [Child] getInitialValue', 'color: LightCoral')

    return window.localStorage.getItem('name') || initialName
  }

  const [name, setName] = useState(getInitialValue)

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

  useEffect(() => {
    console.log('%c    [Child] useEffect', 'color: LightCoral')
    window.localStorage.setItem('name', name)
  }, [name]) // without adding `name` to array `useEffect` will run on every re-render caused by a parent component

  return (
    <section>
      <div>
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} />

        {name && <p>Hello {name}!</p>}
      </div>
    </section>
  )
}

const App = () => {
  console.log('%c[App] render start', 'color: green')

  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount((previousCount) => previousCount + 1)}>
        Render ({count})
      </button>
      <Greeting initialName="George" />
    </>
  )
}
  • Some effects (e.g., subscription) require cleanup to prevent memory leaks. If an effect returns a function, React will run it when it is time to clean up:
const Child = ({}) => {
  console.log('%c    [Child] render', 'color: LightCoral')

  const [value, setValue] = useState(0)

  useEffect(() => {
    console.log('%c    [Child] useEffect start', 'color: LightCoral')

    const intervalId = window.setInterval(() => {
      setValue((v) => v + 1)
    }, [1000])

    return () => {
      console.log('%c    Child: useEffect cleanup', 'color: LightCoral')
      window.clearInterval(intervalId)
    }
  }, [])

  return <div>Interval {value}</div>
}

const App = () => {
  console.log('%c[App] render', 'color: green')

  const [show, setShow] = useState(false)

  return (
    <>
      <button onClick={() => setShow(!show)}>{show ? 'Hide' : 'Show'}</button>
      {show && <Child />}
    </>
  )
}

useMemo

  • Returns a memoized value
  • Used as an optimization to avoid expensive calculations on every render
  • "Give the same value unless arguments have changed"
const computeExpensiveValue = (a) => {
  console.log('%cComputing expensive value...', 'color: LightCoral')

  return a + 42
}

const App = () => {
  const [value, setValue] = useState(0)
  const [count, setCount] = useState(0)

  // this will be called on every re-render
  // const memoizedValue = computeExpensiveValue(value)

  const memoizedValue = useMemo(() => {
    return computeExpensiveValue(value)
  }, [value])

  return (
    <>
      <p>Memoized value: {memoizedValue}</p>
      <div>
        <button onClick={() => setCount(count + 1)}>Render ({count})</button>
        <button onClick={() => setValue(value + 1)}>Increase value</button>
      </div>
    </>
  )
}

useCallback

  • Returns a memoized callback
  • "Don't create a new instance of a function unless arguments have changed"
  • Useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
const List = ({ onItemClick }) => {
  console.log('%c    List: render', 'color: LightCoral')

  return (
    <div>
      {[...Array(5)].map((_, index) => (
        <div
          key={index}
          onClick={() => onItemClick(index)}
        >
          {index} item
        </div>
      ))}
    </div>
  )
}

// this child component is optimized
const MemoList = memo(List)

const App = () => {
  console.log('%c[App] render start', 'color: green')

  const [count, setCount] = useState(0)


  // this will make our optimized child component re-render
  // const handleItemClick = (id) => {
  //   console.log(`%c[App] handleItemClick ${id}`, 'color: green')
  // }

  const handleItemClick = useCallback(
    (id) => {
      console.log(`%c[App] handleItemClick ${id}`, 'color: green')
    },
    []
  )

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Render ({count})</button>
      <MemoList onItemClick={handleItemClick} />
    </>
  )
}

useRef

  • Lets to reference a value that’s not needed for rendering
  • It is like useState without re-rendering
  • Mostly used to access DOM elements
const App = () => {
  const [count, setCount] = useState(0)
  const myRef = useRef(0)

  return (
    <>
      <div>
        {/* changing the current value won't cause a re-render */}
        <button onClick={() => (myRef.current += 1)}>
          Ref: {myRef.current}
        </button>
        <button onClick={() => setCount(count + 1)}>State: {count}</button>
      </div>
    </>
  )
}

useReducer

  • Accepts a reducer - a pure function that accepts state and action and returns a new state
  • Useful for the case when we have complex states that depend on each other
function reducer(state, action) {
  if (action.type === 'loading') {
    return {
      ...state,
      loading: true,
      error: undefined,
      data: undefined,
    }
  }

  if (action.type === 'success') {
    return {
      ...state,
      data: action.data,
      loading: false,
      error: undefined,
    }
  }

  if (action.type === 'error') {
    return {
      ...state,
      loading: false,
      error: action.error,
      data: undefined,
    }
  }
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    data: undefined,
    error: undefined,
  })

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'loading' })
      try {
        const data = await apiCall()
        dispatch({ type: 'success', data })
      } catch (err) {
        dispatch({ type: 'error', error })
      }
    }

    fetchData()
  }, [])

  return null
}

Other APIs

React.memo

  • High order component
  • Returns a memoized component
  • Example: List component in useCallback

React.forwardRef

  • A technique for passing a ref through a component to one of its children
  • Useful when we need to pass ref to our custom React component
const MyCustomInput = ({ ref }) => {
  // We can't pass ref like that
  return <input ref={ref} placeholder="custom input" />
}

const MyCustomInputWithRef = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />
})

const App = () => {
  const nativeInputRef = useRef(null)
  const customInputRef = useRef(null)
  const customInputForwardRef = useRef(null)

  return (
    <div>
      <div>
        <button onClick={() => nativeInputRef.current.focus()}>Focus</button>
        <input ref={nativeInputRef} placeholder="native input" />
      </div>

      {/* this won't work */}
      <div>
        <button onClick={() => customInputRef.current.focus()}>Focus</button>
        <MyCustomInput ref={customInputRef} placeholder="custom input" />
      </div>

      <div>
        <button onClick={() => customInputForwardRef.current.focus()}>
          Focus
        </button>
        <MyCustomInputWithRef
          ref={customInputForwardRef}
          placeholder="forwardRef"
        />
      </div>
    </div>
  )
}

React.lazy

  • Lets to define a component that is loaded dynamically.
  • This helps reduce the bundle size to delay loading components that aren’t used during the initial render.
  • Requires <React.Suspense> component higher in the rendering tree
// This component is loaded dynamicaally
const SomeComponent = React.lazy(() => import('./some-component'))

React.Suspense

  • Lets to specify the loading indicator in case some components in the tree below are not yet ready to render.
  • At the moment only supports lazy loading components
  • Will handle more use cases in the future
// This component is loaded dynamically
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // Displays <Spinner> until OtherComponent loads
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  )
}

Reading

⚠️ **GitHub.com Fallback** ⚠️