Skip to main content

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

· 7 min read

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​

แชร์บทความ
☕

เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว

Buy Me a Coffee
Loading...