reactjs hooks notes

As you learn about hooks, the rabbit hole gets deeper


hooks

  • since react 16.8
  • Hooks rely on JavaScript closures
  • every function inside the component render (including event handlers, effects, timeouts or API calls inside them) captures the props and state of the render call that defined it.
  • "Whenever we update the state, React calls our component. Each render result “sees” its own state value which is a constant inside our function."
    • when you call a setter for a useState then react will call the fn component to run it again, that's a new version of that function with its own closure
    • so a new version of the fn component that is generated with the new state (which is a different state than when the fn was called before)
  • "Inside any particular render, props and state forever stay the same" So too do other functions such as simple handlers and also effects
  • "React synchronizes the DOM according to our current props and state. There is no distinction between a “mount” or an “update” when rendering."
  • ... think of effects in a similar way. useEffect lets you synchronize things outside of the React tree according to our props and state.
  • Thinking in React involves finding the minimal state. This is the same principle, but for updates
  • great article from Dan Abramov


useState()

  • allow create component state
  • pass a callback to perform one off initialization e.g.
    • const [ data, setData ] = useState(() => createData());
  • state value remembered between renders
  • use setter to update the state

useEffect()

  • Effects run after every render and see the props and state from that particular render
    • to opt out of running effect set deps in a dependency array

  • can return function to be called as cleanup function e.g return function cleanup() { ..} to removeEventListeners, unsubscribe etc (could be arrow fn too)
    • cleanup for effect is run from previous Effect before the next Effect is run
  • 2nd parameter (dependency array)

    • none passed - run every render
      • does means effect has access to props and state have latest values
    • empty array passed -  run effect only once on mount and unmount
      • means props and state always have their initial value (could be stale)
    • array of values - run effect when any of the values in array change
      • doesn't run the effect is nothings changed
      • optimize running of the effect
  • you must include in the effects dependency array all the props, state (and anything derived from them) used in the effects code (for useEffect, useMemo, useCallback, useImperativeHandle, useLayoutEffect)
    • i.e. if code in your effect references state or prop then must include in dependency list

    • see more about functions here
    • it's possible for a useEffect calling an api to get out of sync with async responses such that the response displayed does not match the latest dependency value; to mitigate could check the id of the response matches the current and if not, discard the response (could store latest dependency version in useRef to compare later). 
      • Debouncing to reduce frequency of api calls can also help reduce but not eliminate the risk.
    • react is unaware of changes happening outside it's lifecycle and adding such to a dependency array will not cause re-runs when the value changes e.g. a dom useRef as a dependency will not trigger change when dom changes since react only knows what's in react world
    • "Effects always “see” props and state from the render they were defined in"
      • could be missing a dependency if seeing old props/state
    • "An effect’s cleanup function gets invoked every time, right before the execution of the next scheduled effect."

    dependency arrays
    • if you specify deps, all values from inside your component that are used by the effect must be there. Including props, state, functions — anything in your component
      • including derived values from state and props
    • Strategies for dependency arrays
      • ...dependency array to include all the values inside the component that are used inside the effect.
    • dependency array values check: entries in the dependency array are compared to determine if the fn needs to re-run (using Object.is()). 
      • While primitives are compared by value, objects (including functions and arrays with objects) are compared by reference. 
      • And that can be a problem. Oftentimes compare by ref is not what you'd want and won't work as expected, so what to do? consider:
        • try to limit the dependency array to primitives
          • if you have an object which needs to be checked, you could list properties of the object in the dependency array
        • if functions are a dependency then options:
          • move fn outside component if doesn't depend on state/props 
          • try to move inside the effect if only used by the effect, i.e. declare functions needed by effect in the effect itself 
          • otherwise wrap in a useCallback for fn dependencies (memoizing limits fn regeneration and effect rerun)
          • see more about functions and hooks dependencies
        • use a deep comparison like this hook

    useCallback()

    • same function instance is returned between render calls if dependencies are unchanged
    • in some cases its faster/cheaper to not use useCallback and just have regular functions

    useRef()

    • similar to useState but unlike state does not trigger re-render if changes
    • useRef returns an object { current: value } 
      • you can put anything in current, a primitive, object even a function
    • gives you the same object every render (the data in current is unchanged between renders)
      • const myRef = useRef(10);  
      • myRef.current = "1234"
    • ref current is set immediately (not async like setState) e.g. myRef.current = new
    • not just for DOM references; common use cases: to hold ref to dom element, to hold timeout/interval id or an internal count/data structure
      • "Conceptually, you can think of refs as similar to instance variables in a class. "
    • when passed to a DOM element 
      • e.g. <input ref={myRef} .. /> a reference to this dom node is made available to the ref in myRef.current
      • dom uses: e.g. setfocus, measure height, change dom attributes
    • "ref forwarding" allows a parent to pass to a child component a ref for the child to set to to a dom element in it. more here

    useMemo()

    • returns a memoized value, only recalculated when dependency list values change
    • should only be used for expensive operations
    • note: side effects belong in useEffect, not use memo

    React.memo(MyComponent)

    • memoize a functional component
    • best use case: if component is "heavy" ui and the props don't change often (and you've performance profiled)
    • only checks for props changes i.e. given same props will render same result, means react will skip rendering the component and reuse the last result
    • but if the memoized fn uses useState, useReducer or useContext then it will re-render if these change
    • by default comparison is shallow but can provide as 2nd param custom compare

    React hooks rules and side effects (ui.dev)
    API calls, manual DOM manipulation, using browser APIs like localStorage or setTimeout, or anything else that falls outside of simply calculating a View based on props and state is a side effect.


    0. When a component renders, it should do so without running into any side effects
    •     but many times there are side effects, so read on for rules 1 & 2 to handle
    1. If a side effect is triggered by an event, put that side effect in an event handler
    • e.g. update localStorage when user enters data, then put the localStorage side effect in the event handler
    • doing so abstracts it from the code which does rendering (and thus obey rule 0)
    2. If a side effect is synchronizing your component with some external system, put that side effect inside useEffect
    • say you want to load from localStorage when the component is rendered, put that in a useEffect
    • useEffect runs after rendering and thus again obeys rule 0
    • put in the useEffect dependency list any values the effect needs to synchronize



    To avoid passing callbacks down a deep component tree here's a pattern where you create a reducer fn and pass the dispatch down via a Context. The dispatch won't change so won't cause re-renders. If also need to share state then use a separate context for that.

    Comments

    Popular posts from this blog

    deep dive into Material UI TextField built by mui

    angular js protractor e2e cheatsheet

    react-router v6.4+ loaders, actions, forms and more