Skip to content

Instantly share code, notes, and snippets.

@dapids
Last active April 10, 2024 07:26
Show Gist options
  • Save dapids/47b508324fe9c0f152e08cee7d3e5570 to your computer and use it in GitHub Desktop.
Save dapids/47b508324fe9c0f152e08cee7d3e5570 to your computer and use it in GitHub Desktop.
Google Meet Push to Talk
auto-scaling class marp size theme
true
lead
true
58140
default
useEffect: let's adjust the shot! ๐ŸŽฏ
17/12/2019

What is useEffect?

  • useEffect takes effects as input.
  • Effects are functions.
  • Effects are an escape hatch from Reactโ€™s purely functional world.

When does useEffect run?

By default, effects run after every completed render.

useEffect(() => {
  console.log('Effect!')
})

Can we put a limit to it?

We can choose to fire effects only when certain values have changed.

useEffect(() => {
  console.log('Effect!')
}, [])

Golden (but hidden) rule about useEffect

We shouldn't translate components lifecycles into useEffect hooks.

  • The entire web speaks about how to perform this translation.
  • The React doc shows plenty of comparisons between them.
  • It's true that useEffect hooks might run during one or more lifecycles
  • But... this does not mean they should be treated like the same thing! Proof: in SSR componentDidMount does not run. useEffect does.

Lifecycles

class Lifecycles extends Component {
  state = {
    transformedData: null,
  }

  componentWillReceiveProps = ({ data: newData }: { data: string[] }) => {
    const { data } = this.props

    if (data !== newData) {
      const transformedData = doSomethingRealHeavyWithData(newData)

      this.setState({ transformedData })
    }
  }

  render = () => (
    // some JSX here
  )
}

Natural translation

const Hooks = ({ data }: { data: string[] }) => {
  const [transformedData, setTransformedData] = useState(null)

  useEffect(() => {
    const newData = doSomethingRealHeavyWithData(data)

    setTransformedData(newData)
  }, [data])

  return (
    // some JSX here
  )
}

Let's unleash the hooks power!

const BetterHooks = ({ data }: {ย data: string[]ย }) => {
  const transformedData = useMemo(() => (
    doSomethingRealHeavyWithData(data)
  ), [data])

  return (
    // some JSX here
  )
}

Quotes about useEffect dependencies

  • If you specify a list of dependencies as the last argument to useEffect, useMemo, useCallback, it must include all values used inside that participate in the React data flow. - React doc.
  • That includes props, state, and anything derived from them. - React doc.
  • If there is an eslint rule about it, there must be a reason. - lucarge.

Lifecycles

class Lifecycles extends Component {
  state = {
    data: null
  }

  componentDidMount = async ({ input }: { input: string }) => {
    const data = await fetchData(input)

    this.setState({ data })
  }

  render = () => (
    // some JSX here
  )
}

Natural (but wrong) hooks version

const Hooks = ({ input }: { input: string }) => {
  const [data, setData] = useState(null)

  useEffect(() => {
    const run = async () => {
      if (data) {
        return
      }

      const freshData = fetch(input)

      setData(freshData)
    }

    run().catch(() => console.log('Ops!'))
  }, [])

  return (
    // use `data` for rendering something
  )
}

Sound hooks version

const Hooks = ({ input }: { input: string }) => {
  const [data, setData] = useState(null)

  useEffect(() => {
    const run = async () => {
      if (data) {
        return
      }

      const freshData = fetch(input)

      setData(freshData)
    }

    run().catch(() => console.log('Ops!'))
  }, [data, input])

  return (
    // use `data` for rendering something
  )
}

That is not enough!

  • Hooks shouldn't be bound to assumptions related to the status quo of the architecture. ๐Ÿ‘‡๐Ÿป
  • We shouldn't assume that input never changes, just because at the moment it does not happen. ๐Ÿ‘‡๐Ÿป
  • This component could be moved somewhere else or its parent could change its behavior or input could change in the global state or something else in the architecture could make input change.

Sound and solid hooks version

const BetterHooks = ({ input }: { input: string }) => {
  const [data, setData] = useState(null)
  const previousInput = usePrevious(input)

  useEffect(() => {
    const run = async () => {
      if (!!data && input === previousInput) {
        return
      }

      const freshData = fetch(input)

      setData(freshData)
    }

    run().catch(() => console.log('Ops!'))
  }, [data, input, previousInput])

  return (
    // some JSX here
  )
}

What about useAsyncEffect?

If we all agree on always specifying all the hooks dependencies, I'd drop useAsyncEffect since it shadows the exhaustive-deps eslint rule.

Doing without it is not that bad:

useEffect(() => {
  const run = async () => true

  run().then(console.log).catch(console.log)
})

Thanks! ๐Ÿ™๐Ÿป

Questions? ๐Ÿค“

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment