ทำให้ React useEffect hook ไม่ทำงานในการแสดงผลครั้งแรก


95

ตามเอกสาร:

componentDidUpdate()จะถูกเรียกใช้ทันทีหลังจากการอัปเดตเกิดขึ้น วิธีนี้ไม่ถูกเรียกใช้สำหรับการเรนเดอร์เริ่มต้น

เราสามารถใช้useEffect()hook ใหม่เพื่อจำลองcomponentDidUpdate()ได้ แต่ดูเหมือนว่าuseEffect()จะถูกเรียกใช้หลังจากการเรนเดอร์ทุกครั้งแม้ในครั้งแรก ฉันจะทำให้มันไม่ทำงานในการเรนเดอร์ครั้งแรกได้อย่างไร

ดังที่คุณเห็นในตัวอย่างด้านล่างcomponentDidUpdateFunctionถูกพิมพ์ในระหว่างการเรนเดอร์ครั้งแรก แต่componentDidUpdateClassไม่ได้ถูกพิมพ์ในระหว่างการเรนเดอร์ครั้งแรก

function ComponentDidUpdateFunction() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

class ComponentDidUpdateClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidUpdate() {
    console.log("componentDidUpdateClass");
  }

  render() {
    return (
      <div>
        <p>componentDidUpdateClass: {this.state.count} times</p>
        <button
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          Click Me
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <ComponentDidUpdateFunction />
    <ComponentDidUpdateClass />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


1
ฉันขอถามได้ไหมว่า use case คืออะไรเมื่อมันสมเหตุสมผลที่จะทำอะไรบางอย่างตามจำนวนการแสดงผลไม่ใช่ตัวแปรสถานะที่ชัดเจนเช่นcount?
เม.ย. ล้าน

คำตอบ:


111

เราสามารถใช้useRefhook เพื่อเก็บค่าที่ไม่แน่นอนที่เราต้องการได้ดังนั้นเราจึงสามารถใช้มันเพื่อติดตามว่าเป็นครั้งแรกที่useEffectมีการเรียกใช้ฟังก์ชันหรือไม่

หากเราต้องการให้เอฟเฟกต์ทำงานในเฟสเดียวกันcomponentDidUpdateเราสามารถใช้useLayoutEffectแทนได้

ตัวอย่าง

const { useState, useRef, useLayoutEffect } = React;

function ComponentDidUpdateFunction() {
  const [count, setCount] = useState(0);

  const firstUpdate = useRef(true);
  useLayoutEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }

    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

ReactDOM.render(
  <ComponentDidUpdateFunction />,
  document.getElementById("app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


5
ฉันพยายามแทนที่useRefด้วยuseStateแต่การใช้ setter ทำให้เกิด re-render ซึ่งจะไม่เกิดขึ้นเมื่อกำหนดให้firstUpdate.currentดังนั้นฉันเดาว่านี่เป็นวิธีเดียวที่ดี :)
Aprillion

2
มีใครช่วยอธิบายได้ไหมว่าทำไมต้องใช้เอฟเฟกต์เค้าโครงถ้าเราไม่ได้ทำการเปลี่ยนแปลงหรือวัดค่า DOM
ZenVentzi

5
@ZenVentzi ไม่จำเป็นในตัวอย่างนี้ แต่คำถามคือวิธีการเลียนแบบcomponentDidUpdateด้วยตะขอนั่นคือเหตุผลที่ฉันใช้มัน
Tholle

ฉันสร้างเบ็ดที่กำหนดเองที่นี่ตามคำตอบนี้ ขอบคุณสำหรับการใช้งาน!
Patrick Roberts

56

คุณสามารถเปลี่ยนเป็นตะขอที่กำหนดเองได้ดังนี้:

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        if (didMount.current) func();
        else didMount.current = true;
    }, deps);
}

export default useDidMountEffect;

ตัวอย่างการใช้งาน:

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

import useDidMountEffect from '../path/to/useDidMountEffect';

const MyComponent = (props) => {    
    const [state, setState] = useState({
        key: false
    });    

    useEffect(() => {
        // you know what is this, don't you?
    }, []);

    useDidMountEffect(() => {
        // react please run me if 'key' changes, but not on initial render
    }, [state.key]);    

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

2
วิธีนี้แสดงคำเตือนว่ารายการอ้างอิงไม่ใช่ตัวอักษรอาร์เรย์
theprogrammer

1
ฉันใช้เบ็ดนี้ในโปรเจ็กต์ของฉัน แต่ฉันไม่เห็นคำเตือนใด ๆ คุณช่วยให้ข้อมูลเพิ่มเติมได้ไหม
Mehdi Dehghani

1
@vsync คุณกำลังคิดเกี่ยวกับกรณีอื่นที่คุณต้องการเรียกใช้เอฟเฟกต์หนึ่งครั้งในการเรนเดอร์ครั้งแรกและจะไม่เกิดอีกเลย
Programming Guy

2
@vsync ในส่วนบันทึกย่อของreactjs.org/docs/…มีข้อความระบุว่า "หากคุณต้องการเรียกใช้เอฟเฟกต์และล้างข้อมูลเพียงครั้งเดียว (เมื่อเมานต์และยกเลิกการต่อเชื่อม) คุณสามารถส่งอาร์เรย์ว่าง ([]) เป็น a ข้อโต้แย้งที่สอง " สิ่งนี้ตรงกับพฤติกรรมที่สังเกตได้สำหรับฉัน
Programming Guy

5

ฉันทำuseFirstRenderเบ็ดง่ายๆเพื่อจัดการกับเคสเช่นเน้นการป้อนแบบฟอร์ม:

import { useRef, useEffect } from 'react';

export function useFirstRender() {
  const firstRender = useRef(true);

  useEffect(() => {
    firstRender.current = false;
  }, []);

  return firstRender.current;
}

เริ่มต้นด้วยจากtrueนั้นเปลี่ยนเป็นfalseในไฟล์useEffectซึ่งทำงานเพียงครั้งเดียวและไม่ดำเนินการอีกเลย

ในส่วนประกอบของคุณให้ใช้:

const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);

useEffect(() => {
  if (firstRender || errors.phoneNumber) {
    phoneNumberRef.current.focus();
  }
}, [firstRender, errors.phoneNumber]);

สำหรับกรณีของคุณคุณจะใช้if (!firstRender) { ....


3

@ravi คุณไม่ได้เรียกใช้ฟังก์ชันยกเลิกการต่อเชื่อมที่ส่งผ่าน นี่คือเวอร์ชันที่สมบูรณ์กว่าเล็กน้อย:

/**
 * Identical to React.useEffect, except that it never runs on mount. This is
 * the equivalent of the componentDidUpdate lifecycle function.
 *
 * @param {function:function} effect - A useEffect effect.
 * @param {array} [dependencies] - useEffect dependency list.
 */
export const useEffectExceptOnMount = (effect, dependencies) => {
  const mounted = React.useRef(false);
  React.useEffect(() => {
    if (mounted.current) {
      const unmount = effect();
      return () => unmount && unmount();
    } else {
      mounted.current = true;
    }
  }, dependencies);

  // Reset on unmount for the next mount.
  React.useEffect(() => {
    return () => mounted.current = false;
  }, []);
};


สวัสดี @Whatabrain วิธีใช้ hook ที่กำหนดเองนี้ในการส่งผ่านรายการที่ไม่ต้องพึ่งพา ไม่ใช่ช่องว่างซึ่งจะเหมือนกับ componentDidmount แต่มีบางอย่างเช่นuseEffect(() => {...});
KevDing

1
@KevDing ควรจะง่ายพอ ๆ กับการละเว้นdependenciesพารามิเตอร์เมื่อคุณเรียกมัน
Whatabrain

1

@MehdiDehghani การทำงานวิธีการแก้ปัญหาของคุณสมบูรณ์ดีหนึ่งนอกจากนี้คุณต้องทำคือการยกเลิกการต่อเชื่อมบน, รีเซ็ตมูลค่าให้กับdidMount.current falseเมื่อใดที่จะพยายามใช้เบ็ดที่กำหนดเองนี้ที่อื่นคุณจะไม่ได้รับค่าแคช

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        let unmount;
        if (didMount.current) unmount = func();
        else didMount.current = true;

        return () => {
            didMount.current = false;
            unmount && unmount();
        }
    }, deps);
}

export default useDidMountEffect;

ฉันไม่แน่ใจว่านี่เป็นสิ่งที่จำเป็นเนื่องจากถ้าส่วนประกอบนั้นสิ้นสุดการถอนการต่อเชื่อมเพราะถ้ามันไม่ติดตั้งใหม่ didMount จะเริ่มต้นfalseใหม่
Cameron Yick
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.