React Hooks - useEffect

jh22j9·2020년 11월 23일

React Hooks

목록 보기
3/3

useEffect


  • The Effect Hook lets you perform side effects in functioinal components.
  • It is a close replacement for componentDidMount, componentDidUpdate and componentWillUnmount.

How to use the effect hook?


Class Component

// ClassCounterOne.js
import React, { Component } from 'react'

export class ClassCounterOne extends Component {
  constructor(props) {
    super(props)
  
    this.state = {
       count: 0
    }
  }
  
  componentDidMount() {
    // render initial state
    document.title = `Clicked ${this.state.count} times`
  }

  componentDidUpdate(prevProps, prevState) {
    // render updated state
    document.title = `Clicked ${this.state.count} times`
  }

  render() {
    console.log('render')
    return (
      <div>
        <button onClick={() => this.setState({count: this.state.count + 1})}>
          Click {this.state.count} times
        </button>
      </div>
    )
  }
}

export default ClassCounterOne

→ Functional Component

// HookCounterOne.js
import React, { useState, useEffect } from 'react'

function HookCounterOne() {

  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
    console.log('useEffect runs')
  })

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click {count} times</button>
    </div>
  )
}

export default HookCounterOne

When we specify useEffect, we are basically requesting react to execute the function that is passed as an argument every time the component renders. So useEffect runs after every render of the component.

It runs both after the first render(componentDidMount) and after every update(componentDidUpdate).

Conditionnally Run Effects

In some cases, applying the effect after every render might create a performance problem.

// ClassCounterOne.js
import React, { Component } from 'react'

export class ClassCounterOne extends Component {
  constructor(props) {
    super(props)
  
    this.state = {
       count: 0,
       name: ''
    }
  }
  
  componentDidMount() {
    document.title = `Clicked ${this.state.count} times`
  } 

  componentDidUpdate(prevProps, prevState) {
      console.log('Updating document title')
      document.title = `Clicked ${this.state.count} times`
  }

  render() {
    return (
      <div>
        <input 
          type="text" 
          value={this.state.name} 
          onChange={e => {
            this.setState({name: e.target.value})
          }} 
        />
        <button onClick={() => this.setState({count: this.state.count + 1})}>
          Click {this.state.count} times
        </button>
      </div>
    )
  }
}

export default ClassCounterOne

위 코드를 실행하여 콘솔을 확인하면, Updating document title이 버튼을 클릭했을 때 뿐만 아니라 input창에 이름을 입력할 때도 로그되는 것을 확인할 수 있다. 이름을 입력하는 input창에 onChange 이벤트가 일어날 때마다 componentDidUpdate를 불필요하게 호출하며 document.title을 업데이트 하고 있는 것이다.

To optimize this, we can compare the current value before and after the update. And if at all the current value changed, we then conditionnaly update the title.

componentDidUpdate(prevProps, prevState) {
  if(prevState.count !== this.state.count) {
    console.log('Updating document title')
    document.title = `Clicked ${this.state.count} times`
  }
}

How to implement the same with functional component and the useEffect hook ?

// HookCounterOne.js
import React, { useState, useEffect } from 'react'

function HookCounterOne() {

  const [count, setCount] = useState(0)
  const [name, setName] = useState('')

  useEffect(() => {
    console.log('useEffect - updating document title')
    document.title = `You clicked ${count} times`
  }, [count])

  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>Click {count} times</button>
    </div>
  )
}

export default HookCounterOne

For conditionnaly excuting an effect, we passing a second parameter. This parameter is an array. Within this array, we need to specify either props or state that we need to watch for. Only if those props or state specified in this area were to change, the effect would be executed.

For our example, we need the effect to be executed only when the count value changes.

The second parameter is the array of values that the effect depends on.

Run Effects Only Once


How to run an effect only once ? How to mimic componentDidMount with useEffect and functional component ?

import React, { Component } from 'react'

class ClassMouse extends Component {
  constructor(props) {
    super(props)
  
    this.state = {
       x: 0,
       y: 0
    }
  }
  
  logMousePosition = e => {
    console.log('logMousePosition')
    this.setState({x: e.clientX, y: e.clientY})
  }

  componentDidMount() {
    console.log('componentDidMount')
    window.addEventListener('mousemove', this.logMousePosition)
  }

  render() {
    return (
      <div>
        X- {this.state.x} Y = {this.state.y}
      </div>
    )
  }
}

export default ClassMouse

Implement the same with useEffect and functional component.

import React, {useState, useEffect} from 'react'

function HookMouse() {

  const [x, setX] = useState(0)
  const [y, setY] = useState(0)

  const logMousePosition = e => {
    console.log('Mouse event')
    setX(e.clientX)
    setY(e.clientY)
  }

  useEffect(() => {
    console.log('useEffect called')
    window.addEventListener('mousemove', logMousePosition)
  })

  return (
    <div>
       Hooks X - {x} Y - {y}
    </div>
  )
}
 
export default HookMouse

위 코드를 실행하면 마우스를 움직일 때마다 logMousePosition 함수만 실행되는 것이 아니라 불필요하게 useEffect까지 호출되어 useEffect called가 로그된다. addEventListner는 처음에 한 번만 실행되면 된다.

For our example, we don't really want the effect to depend on anything, we want it to be called once on initial render only, and the way we achieve that is by simply specifying an empty array as the second parameter to useEffect.

  useEffect(() => {
    console.log('useEffect called')
    window.addEventListener('mousemove', logMousePosition)
  }, [])

useEffect with Cleanup


How to make use of the componentWillUnmount functionality that is possible with useEffect ?

// App.js
import React from 'react';
import MouseContainer from './MouseContainer';

function App() { 
  return (
    <div className="App">
      <MouseContainer />
    </div>
  );
}
 
export default App; 
// MouseContainer.js
import React, {useState} from 'react'
import HookMouse from './HookMouse'

function MouseContainer() {
  const [display, setDisplay] = useState(true)

  return (
    <div>
      <button onClick={() => setDisplay(!display)}>Toggle display</button>
      {display && <HookMouse />}
    </div>
  )
}

export default MouseContainer

toggle display 버튼을 클릭하면 display의 값이 false가 되면서 HookMouse 컴포넌트가 언마운트 된다. 그런데 이 상태에서 마우스를 다시 움직이면 콘솔창에서 warning을 보게 된다.

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.

그리고 HookMouse 컴포넌트의 log statement인 Mouse event가 다시 로그되는 것을 볼 수 있다.

So event though the component has been removed, the event listener which belongs to that component is still listening and executing. This is what the warning indicates as well.

When you unmount component, make sure you cancel all your subscriptions and listeners.

Now, how do we handle the cleanup code ?

With class component, we had the componentWillUnmount :

  componentWillUnmount() {
    window.removeEventListener('mousemove', this.logMousePosition)
  }

How do we mimic this life cycle functionality in useEffect?

The function that is passed to useEffect can return a function which will be executed when the component will unmount.

// HookMouse.js
  useEffect(() => {
    console.log('useEffect called')
    window.addEventListener('mousemove', logMousePosition)
    
    return () => {
      console.log('component unmounting code')
      window.removeEventListener('mousemove', logMousePosition)
    }
  }, [])

So when you want to execute some component cleanup code, you include it in a function and return that function from the function passed to useEffect.


🔗 React Hooks Tutorial - 6 - useEffect Hook

0개의 댓글