Skip to main content

React ถ้าใช้ useEffect ควรเขียน Cleanup functions วิธีใช้งานแบบผิด ๆ ที่มือใหม่อาจจะพลาด

Kongvut Sangkla

Intro

  • StrictMode problems
  • useEffect and deps problems
  • Primitive and Non-Primitive
  • Cleanup function

Basic of useEffect

ตามปกติแล้ว useEffect() คือ React Hooks ตัวหนึ่งที่จะทำงานโดยขึ้นอยู่กับ Dependency (deps)

นั่นหมายความว่า ถ้าค่าของ deps มีการเปลี่ยนแปลง useEffect ก็จะทำงาน

Live Editor
() => {
  const [number, setNumber] = useState(0)
  const [name, setName] = useState("")

  //ที่ผิด (ไม่กำหนด Dependency)
  // useEffect(() => {
  //   console.count("useEffect runs!")
  //   document.title = `You clicked ${number} times`
  // })

  //ที่ถูก
  useEffect(() => {
    console.count("useEffect runs!")
    document.title = `You clicked ${number} times`
  }, [number])

  console.count("component rendered!")

  return (
    <>
      <span>You clicked {number} times</span>
      <input
        onChange={(e) => setName(e.target.value)}
        type="text"
        placeholder="enter a name..."
      />
      <button onClick={() => setNumber(number + 1)}>Increase</button>
    </>
  )
}
Result
Loading...

Primitive vs. Non-Primitive data types

วิธีด้านบนเป็นการใช้งาน useEffect แบบพื้นฐานที่ถูกต้อง (แต่ก็ยังไม่ใช่วิธีที่ดีที่สุด) แต่ก่อนที่จะมาดูวิธีที่ดีที่สุด มาทำความเข้าใจประเภทของตัวแปรแบบ Primitive and Non-Primitive data types กันก่อน

Primitive

  • string
  • number
  • booleans
  • null
  • undefined
  • bigint
  • symbol

Non-Primitive

  • object
  • arrays
  • function

Primitive vs. Non-Primitive

Primitive

  • Primitive values มีคุณสมบัติ immutable
  • Primitive สามารถใช้ values compared ด้วย value
  • Primitive Data types เป็น predefined
  • Primitive Data types จะมีการกำหนด values

Non-Primitive

  • Non-Primitive values มีคุณสมบัติ mutable
  • Non-Primitive เก็บ values ด้วย Address
  • Non-Primitive สามารถ compare ด้วย reference
Primitive vs. Non-Primitive
//Primitive data types String, number, booleans, null, undefined
const a = "Pikachu"
const b = "Pikachu"
a === b //true
a === "Pikachu" //true

"Pikachu" === "Pikachu" //true
const c = 1
const d = 1
c === d //true
d === 1 //true

true === true //true
false === false //true
null === null //true
undefined === undefined //true
null === undefined //false

//Non-Primitive Object, arrays
const x = { name: "Pikachu" }
const y = { name: "Pikachu" }
x === y //false

const z = y
z === y //true

[] === []//false
[1] === [1]//false
1 === 1//true

Best practices How to use useEffect

จากหัวข้อที่แล้ว Basic of useEffect ทำให้เรารู้ว่า useEffect ทำงานขึ้นอยู่กับค่า Dependency (deps) แต่ deps ก็เป็นได้ทั้ง Primitive and Non-Primitive data types ที่นี้มาดูวิธีการใช้ useEffect ที่ดีที่สุด

Live Editor
() => {
    const [name, setName] = useState("")
    const [state, setState] = useState({
        name: "",
        selected: false,
        phone: "0123456789",
        age: 30
    })

    // วิธีที่ผิด 
    // เพราะ state คือ Non-Primitive ที่ไม่มีคุณสมติ Memoized
    // useEffect(() => {
    //   console.log(`The state has changed, useEffect runs!`)
    // }, [state])

    // วิธีที่ถูก 1
    // ใช้ useMemo ที่มีคุณสมบัติ Memoized เข้ามาช่วยจำค่าใน states
    const user = useMemo(
        () => ({
            name: state.name,
            selected: state.selected,
        }),
        [state.name, state.selected]
    )

    // user คือ Non-Primitive
    // กำหนดเป็น deps ได้ เพราะว่ามีคุณสมบบัติ Memoized ในตัวแล้ว
    useEffect(() => {
      console.log(`The state has changed, useEffect runs!`)
    }, [user])

    // วิธีที่ถูก 2
    // คือการกำหนด Primitive เข้าไปตรง ๆ
    // useEffect(() => {
    //     console.log(`The state has changed, useEffect runs!`)
    // }, [state.name, state.selected])

    const handleAddName = () => {
        setState((prev) => ({ ...prev, name }))
    }

    const handleSelect = () => {
        setState((prev) => ({ ...prev, selected: true }))
    }

    return (
        <>
            <input type="text" onChange={(e) => setName(e.target.value)} />
            <button onClick={handleAddName}>Add Name</button>
            <button onClick={handleSelect}>Select</button>
            {`{
                name:${state.name},
                selected:${state.selected.toString()}
            }`}
        </>
    )
}
Result
Loading...
tip

สรุปก็คือ การกำหนดค่า Dependency ให้กับ useEffect ควรเป็น Data type แบบ

  • Primitive จะดีที่สุด เพื่อมั่นใจว่า useEffect จะทำงานได้ถูกต้องที่ควรจะเป็น
  • Non-Primitive ก็สามารถใช้ได้ (แต่ต้องมีคุณบัติ Memoized ด้วย)

อ่านเนื้อหาเพิ่มเติมเกี่ยวกับการใช้งาน useMemo และ useCallback อย่างละเอียดได้ที่นี่ "การใช้งานและความแตกต่างระหว่าง useMemo และ useCallback ของ React Hooks"

useEffect with Cleanup function

ใช้ useEffect รวมกับการทำ Cleanup function คือการทำให้มั่นใจว่า Statements ใดใน useEffect จะมีการทำงานแค่ 1 รอบ

Syntax
useEffect(() => {
// Your effect
return () => {
// Cleanup
}
}, [value])

การใช้งาน useEffect และเพิ่ม Cleanup function

Live Editor
() => {
    const [number, setNumber] = useState(0)

    // วิธีที่ผิด
    // อย่าอัปเดต state โดยการเรียกตัวมันเอง
    // แม้ว่ากำหนด deps เป็น number แล้วก็ตาม
    // จะเกิด useEffect runs มหาศาล (ทำให้หน้า App หน่วงให้ระวังข้อนี้)
    // useEffect(() => {
    //   console.count("useEffect runs")
    //   setInterval(() => {
    //     setNumber(number + 1)
    //   }, 1000)
    // }, [number])

    // วิธีที่ถูก
    // useEffect runs 1 ครั้ง
    // แต่ยังไม่ได้มี Cleanup function
    // useEffect(() => {
    //   console.count("useEffect runs")
    //   setInterval(() => {
    //     setNumber(number + 1)
    //   }, 1000)
    // }, [])

    // วิธีที่ถูก
    // มี Cleanup function
    useEffect(() => {
        console.count("useEffect runs")
        const interval = setInterval(() => {
            setNumber(number + 1)
        }, 1000)

        // หลักการคือ
        // ต้องมีตัวแปรที่เก็บสถานะ ในตัวอย่างคือ interval สำหรับ functions ใด ๆ ก็ตามที่ทำงานใน useEffect
        // จากนั้นให้มี 1 function ที่ทำหน้าที่ return ตัวแปรที่เป็นสถานะ (ที่เปลี่ยนแล้ว)
        return () => {
            clearInterval(interval)
        }
    }, [])

    return <>{number}</>
}
Result
Loading...

How cleanup function work

ลำดับการทำงาน

  1. useEffect runs!
  2. useEffect runs2!
  3. useEffect runs3!
  4. Wait! before running the effect
  5. Okey done! You can run
Live Editor
() => {
    const [toggle, setToggle] = useState(false)
    
    useEffect(() => {
        console.log("useEffect runs!")
        console.log("useEffect runs2!")
        console.log("useEffect runs3!")
        
        return () => {
            console.log(
                "Wait! before running the effect"
            )
            //clear the previous useEffect.
            console.log(
                "Okey done! You can run"
            )
        }
    }, [toggle])

    return (
        <div>
            <button onClick={() => setToggle(!toggle)}>Toggle</button>
        </div>
    )
}
Result
Loading...

When should we use the cleanup function?

เมื่อใดที่เราควรใช้ cleanup function

  • ใช้ทำ Unsubscribed หลักการคือสร้างตัวแปรเพื่อกำหนดสถานะการทำงาน จากนั้น return สถานะที่ตรงข้ามกับสถานะเริ่มต้น
  • ใช้เพื่อ Abort request (Cancelling) อื่น ๆ ที่ยังทำงานอยู่ เช่น เมื่อเรากดไปที่หน้าเพจใหม่ก็ให้ยกเลิก requests ก่อนหน้า (หมายถึงถ้า request นั้นยังทำงานไม่เสร็จ)

Unsubscribed

() => {
//Cleanup with Unsubscribed
useEffect(() => {
let subscribed = true

// Some work/process function
;(() => {
// Process with some logic
// ...
// and then check if subscribed === true
if (subscribed) {
console.log("work is ready!")
//setData(data)
//console.log(data)
}
})()

// Cleanup function
// and set subscribed = false
return () => {
console.log("cancelled!")
subscribed = false
}
}, [])

return (
<>
...
</>
)
}

Abort request

Abort - With Fetch()

Cleanup function ด้วยวิธี Abort request กับ Fetch() ซึ่งเป็น Native method ของ JS

() => {
const [posts, setPosts] = useState({})

useEffect(() => {
// Create AbortController and get signal
const controller = new AbortController()
const signal = controller.signal

// fetch with signal
fetch(`https://jsonplaceholder.typicode.com/posts`, { signal })
.then((res) => res.json())
.then((data) => {
setPosts(data)
})
.catch((err) => {
if (err === "AbortError") {
console.log("Request canceled!")
} else {
//todo:handle error
}
})

// Cleanup function with controller abort
return () => {
controller.abort()
}
}, [])

return (
<>
...
</>
)
}

Abort - With Axios

Cleanup function ด้วยวิธี Abort request กับ Library ของ Axios

import axios from "axios"

() => {
const [posts, setPosts] = useState({})

useEffect(() => {
// Create AbortController and get signal
const controller = new AbortController()

//axios.get() with signal config
axios
.get(`https://jsonplaceholder.typicode.com/posts`, {
signal: controller.signal,
})
.then((res) => {
setPosts(res.data)
})
.catch((err) => {
if (axios.isCancel(err)) {
console.log("Request canceled!")
} else {
//todo:handle error
}
})

// Cleanup function with controller abort
return () => {
controller.abort()
}
}, [])

return (
<>
...
</>
)
}

Summary

สรุปคือ

  • Cleanup function เป็นการกำหนด status ค่าบางอย่างที่บอกให้รู้ว่าได้ทำงานแล้ว (เพื่อไม่ให้ทำงานซ้ำอีกรอบ)
  • React StrictMode จะเกิด Re-render 2 ครั้ง ถ้าใช้ UseEffect ก็จะถูกเรียก 2 ครั้ง เป็นเรื่องปกติ
  • ขณะใช้ StrictMode การเขียน Cleanup function จะสามารถแก้ไขปัญหาและทำให้ UseEffect ทำงานแค่ 1 ครั้ง
  • ตัวแปรประเภท Primitive vs. Non-Primitive มีผลในการกำหนด deps ของ UseEffect
  • เวลาใช้ useEffect ไม่เขียน Cleanup function ก็ได้ใช่ไหม ? = ได้ แต่ควรเขียนดีกว่า
  • ถ้าไม่ได้เขียน Cleanup function แล้วใช้ StrictMode ในขณะ dev
    • เมื่อใช้ useEffect จะทำงาน 2 ครั้ง
    • แต่เมื่อ build เป็น Production แล้วก็จะทำงานแค่ 1 ครั้ง
    • แต่สุดท้ายแล้วก็จะมีปัญหาเรื่อง Component unmounted, Re-render และการ Set states ที่ไม่จำเป็น
  • คำตอบสุดท้ายคือควรเขียน Cleanup function อยู่ดี 😄

ขอบคุณที่อ่านจนจบครับ 😊

References

Loading...