การเรียกใช้ตัวอัปเดตสถานะจาก useState ในคอมโพเนนต์หลายครั้งทำให้เกิดการแสดงผลซ้ำหลายครั้ง


122

ฉันกำลังลอง React hooks เป็นครั้งแรกและทุกอย่างดูเหมือนจะดีจนกระทั่งฉันรู้ว่าเมื่อฉันได้รับข้อมูลและอัปเดตตัวแปรสถานะที่แตกต่างกันสองตัว (ข้อมูลและแฟล็กการโหลด) ส่วนประกอบของฉัน (ตารางข้อมูล) จะแสดงผลสองครั้งแม้ว่าทั้งสองสาย ไปยังตัวอัปเดตสถานะกำลังเกิดขึ้นในฟังก์ชันเดียวกัน นี่คือฟังก์ชัน api ของฉันซึ่งส่งคืนตัวแปรทั้งสองไปยังส่วนประกอบของฉัน

const getData = url => {

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {

        const test = await api.get('/people')

        if(test.ok){
            setLoading(false);
            setData(test.data.results);
        }

    }, []);

    return { data, loading };
};

ในคอมโพเนนต์คลาสปกติคุณจะโทรเพียงครั้งเดียวเพื่ออัปเดตสถานะซึ่งอาจเป็นอ็อบเจ็กต์ที่ซับซ้อน แต่ดูเหมือนว่า "hooks way" จะแบ่งสถานะออกเป็นหน่วยย่อย ๆ ซึ่งเป็นผลข้างเคียงที่ดูเหมือนว่าจะเกิดขึ้นอีกหลายครั้ง แสดงผลเมื่อมีการอัปเดตแยกกัน มีความคิดอย่างไรที่จะลดปัญหานี้?


หากคุณต้องพึ่งพารัฐคุณควรใช้useReducer
thinklinux

ว้าว! ฉันเพิ่งค้นพบสิ่งนี้และมันทำให้ฉันเข้าใจอย่างสมบูรณ์เกี่ยวกับวิธีการแสดงผลปฏิกิริยา ฉันไม่เข้าใจข้อได้เปรียบใด ๆ สำหรับการทำงานในลักษณะนี้ - ดูเหมือนว่าโดยพลการที่พฤติกรรมในการโทรกลับแบบ async จะแตกต่างจากในตัวจัดการเหตุการณ์ปกติ BTW ในการทดสอบของฉันดูเหมือนว่าการกระทบยอด (เช่นการอัปเดตของ DOM จริง) จะไม่เกิดขึ้นจนกว่าจะมีการประมวลผลการเรียก setState ทั้งหมดดังนั้นการเรียกใช้การแสดงผลระดับกลางจึงสูญเปล่า
Andy

คำตอบ:


122

คุณสามารถรวมloadingสถานะและdataสถานะเป็นออบเจ็กต์สถานะเดียวจากนั้นคุณสามารถทำการsetStateเรียกหนึ่งครั้งและจะมีการแสดงผลเพียงครั้งเดียว

หมายเหตุ:ไม่เหมือนกับsetStateคอมโพเนนต์ในคลาสสิ่งที่setStateส่งคืนจากuseStateไม่ผสานอ็อบเจกต์กับสถานะที่มีอยู่มันจะแทนที่อ็อบเจ็กต์ทั้งหมด หากคุณต้องการทำการผสานคุณจะต้องอ่านสถานะก่อนหน้านี้และรวมเข้ากับค่าใหม่ด้วยตัวคุณเอง อ้างถึงเอกสาร

ฉันจะไม่กังวลมากเกินไปเกี่ยวกับการแสดงผลการโทรมากเกินไปจนกว่าคุณจะพิจารณาว่าคุณมีปัญหาด้านประสิทธิภาพ การแสดงผล (ในบริบทการตอบสนอง) และการคอมมิตการอัปเดต DOM เสมือนไปยัง DOM จริงนั้นเป็นคนละเรื่องกัน การแสดงผลในที่นี้หมายถึงการสร้าง DOM เสมือนไม่ใช่เกี่ยวกับการอัปเดต DOM ของเบราว์เซอร์ การตอบสนองอาจแบตช์การsetStateโทรและอัปเดต DOM ของเบราว์เซอร์ด้วยสถานะใหม่ขั้นสุดท้าย

const {useState, useEffect} = React;

function App() {
  const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    // Note that this replaces the entire object and deletes user key!
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, 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>

ทางเลือก - เขียน hook การควบรวมกิจการของคุณเอง

const {useState, useEffect} = React;

function useMergeState(initialState) {
  const [state, setState] = useState(initialState);
  const setMergedState = newState => 
    setState(prevState => Object.assign({}, prevState, newState)
  );
  return [state, setMergedState];
}

function App() {
  const [userRequest, setUserRequest] = useMergeState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, 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
ฉันยอมรับว่ามันไม่เหมาะ แต่ก็ยังเป็นวิธีที่สมเหตุสมผล คุณสามารถเขียนเบ็ดแบบกำหนดเองของคุณเองซึ่งจะทำการผสานหากคุณไม่ต้องการทำการรวมเอง ฉันปรับปรุงประโยคสุดท้ายให้ชัดเจนขึ้น คุณคุ้นเคยกับวิธีการทำงานของ React หรือไม่? ถ้าไม่ได้นี่คือการเชื่อมโยงบางอย่างที่อาจช่วยให้คุณเข้าใจดีกว่า - reactjs.org/docs/reconciliation.html , blog.vjeux.com/2013/javascript/react-performance.html
Yangshun Tay

2
@jonhobbs ฉันได้เพิ่มคำตอบอื่นซึ่งสร้างuseMergeStateเบ็ดเพื่อให้คุณสามารถรวมสถานะโดยอัตโนมัติ
Yangshun Tay

1
สุดยอดขอบคุณอีกครั้ง ฉันคุ้นเคยกับการใช้ React มากขึ้น แต่สามารถเรียนรู้ได้มากเกี่ยวกับการใช้งานภายใน ฉันจะให้พวกเขาอ่าน
jonhobbs

2
อยากรู้อยากเห็นภายใต้สถานการณ์ใดที่อาจเป็นจริง: "การตอบสนองอาจรวมการsetStateโทรและอัปเดต DOM ของเบราว์เซอร์ด้วยสถานะใหม่ขั้นสุดท้าย "
ecoe

1
ดี .. ฉันคิดเกี่ยวกับการรวมสถานะหรือใช้สถานะ payload แยกต่างหากและให้รัฐอื่นอ่านจากวัตถุน้ำหนักบรรทุก โพสต์ที่ดีมากแม้ว่า 10/10 จะอ่านอีกครั้ง
Anthony Moon Beam Toorie

63

นอกจากนี้ยังมีวิธีแก้ปัญหาอื่นโดยใช้useReducer! setStateครั้งแรกที่เรากำหนดใหม่ของเรา

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)

หลังจากนั้นเราก็สามารถใช้มันเหมือนกับคลาสเก่า ๆ ที่ดีthis.setStateโดยไม่มีthis!

setState({loading: false, data: test.data.results})

ดังที่คุณสังเกตเห็นในใหม่ของเราsetState(เช่นเดียวกับที่เราเคยมีมาก่อนหน้านี้this.setState) เราไม่จำเป็นต้องอัปเดตสถานะทั้งหมดด้วยกัน! ตัวอย่างเช่นฉันสามารถเปลี่ยนสถานะของเราได้เช่นนี้ (และจะไม่เปลี่ยนสถานะอื่น!):

setState({loading: false})

น่ากลัวฮ่า?!

ลองรวบรวมชิ้นส่วนทั้งหมดเข้าด้วยกัน:

import {useReducer} from 'react'

const getData = url => {
  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}

Expected 0 arguments, but got 1.ts(2554)เป็นข้อผิดพลาดที่ฉันได้รับเมื่อพยายามใช้ useReducer ด้วยวิธีนั้น อย่างแม่นยำมากsetStateขึ้น คุณจะแก้ไขได้อย่างไร? ไฟล์เป็น js แต่เรายังใช้การตรวจสอบ ts
ZenVentzi

ขอบคุณมากขอบคุณ
Nisharg Shah

ฉันคิดว่ามันยังคงเป็นความคิดเดียวกันกับที่สองรัฐรวมเข้าด้วยกัน แต่ในบางกรณีคุณไม่สามารถรวมรัฐได้
user1888955

45

การอัปเดตแบทช์ใน react-hooks https://github.com/facebook/react/issues/14259

การตอบสนองในขณะนี้จะอัปเดตสถานะเป็นกลุ่มหากทริกเกอร์จากภายในเหตุการณ์ที่อิงตามการตอบสนองเช่นการคลิกปุ่มหรือการเปลี่ยนแปลงอินพุต จะไม่อัปเดตเป็นกลุ่มหากถูกทริกเกอร์ภายนอกตัวจัดการเหตุการณ์ตอบสนองเช่นการโทรแบบไม่ซิงค์


5
นี่เป็นคำตอบที่มีประโยชน์เนื่องจากสามารถแก้ปัญหาได้ในกรณีส่วนใหญ่โดยไม่ต้องทำอะไรเลย ขอบคุณ!
davnicwil


4

หากคุณใช้ hooks ของบุคคลที่สามและไม่สามารถรวมสถานะเป็นวัตถุเดียวหรือใช้งานuseReducerได้วิธีแก้ปัญหาคือใช้:

ReactDOM.unstable_batchedUpdates(() => { ... })

แนะนำโดย Dan Abramov ที่นี่

ดูตัวอย่างนี้


เกือบหนึ่งปีต่อมาคุณลักษณะนี้ดูเหมือนจะไม่มีเอกสารทั้งหมดและยังคงถูกทำเครื่องหมายว่าไม่เสถียร :-(
Andy

4

ในการจำลองthis.setStateพฤติกรรมการผสานจากส่วนประกอบของคลาส React docs แนะนำให้ใช้รูปแบบการทำงานของการuseStateแพร่กระจายวัตถุ - ไม่จำเป็นสำหรับuseReducer:

setState(prevState => {
  return {...prevState, loading, data};
});

ตอนนี้ทั้งสองสถานะรวมเป็นหนึ่งซึ่งจะช่วยให้คุณประหยัดรอบการแสดงผล

มีประโยชน์อื่นที่มีวัตถุรัฐหนึ่งคือloadingและdataมีขึ้นอยู่กับรัฐ การเปลี่ยนแปลงสถานะที่ไม่ถูกต้องจะชัดเจนยิ่งขึ้นเมื่อรวมรัฐเข้าด้วยกัน:

setState({ loading: true, data }); // ups... loading, but we already set data

คุณได้ดียิ่งขึ้นสามารถให้แน่ใจว่าการรัฐที่สอดคล้องกัน โดย 1) การทำสถานะ - loading, success, errorฯลฯ - อย่างชัดเจนในรัฐและ 2 ของคุณ) โดยใช้useReducerตรรกะรัฐแค็ปซูลในการลดนี้:

const useData = () => {
  const [state, dispatch] = useReducer(reducer, /*...*/);

  useEffect(() => {
    api.get('/people').then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // keep state consistent, e.g. reset data, if loading
  else if (status === "loading") return { ...state, data: undefined, status };
  return state;
};

const App = () => {
  const { data, status } = useData();
  return status === "loading" ? <div> Loading... </div> : (
    // success, display data 
  )
}

PS: อย่าลืมเติมคำนำหน้า Hooks แบบกำหนดเองด้วยuse( useDataแทนgetData) ผ่านไปยังโทรกลับไปไม่สามารถuseEffectasync


1
โหวตแล้ว! นี่เป็นหนึ่งในคุณสมบัติที่มีประโยชน์ที่สุดที่ฉันพบใน React API พบเมื่อฉันพยายามจัดเก็บฟังก์ชันเป็นสถานะ (ฉันรู้ว่ามันแปลกที่จะจัดเก็บฟังก์ชัน แต่ฉันต้องมีเหตุผลบางอย่างที่ฉันจำไม่ได้) และมันไม่ได้ผลตามที่คาดไว้เนื่องจากลายเซ็นการโทรที่สงวนไว้นี้
elquimista

1

คำตอบเพิ่มเติมเล็กน้อยhttps://stackoverflow.com/a/53575023/121143

เย็น! สำหรับผู้ที่วางแผนจะใช้ hook นี้สามารถเขียนด้วยวิธีที่มีประสิทธิภาพเล็กน้อยในการทำงานกับฟังก์ชันเป็นอาร์กิวเมนต์เช่นนี้:

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newState =>
    typeof newState == "function"
      ? setState(prevState => ({ ...prevState, ...newState(prevState) }))
      : setState(prevState => ({ ...prevState, ...newState }));
  return [state, setMergedState];
};

อัปเดต : เวอร์ชันที่ปรับให้เหมาะสมสถานะจะไม่ถูกแก้ไขเมื่อไม่มีการเปลี่ยนแปลงสถานะบางส่วนที่เข้ามา

const shallowPartialCompare = (obj, partialObj) =>
  Object.keys(partialObj).every(
    key =>
      obj.hasOwnProperty(key) &&
      obj[key] === partialObj[key]
  );

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newIncomingState =>
    setState(prevState => {
      const newState =
        typeof newIncomingState == "function"
          ? newIncomingState(prevState)
          : newIncomingState;
      return shallowPartialCompare(prevState, newState)
        ? prevState
        : { ...prevState, ...newState };
    });
  return [state, setMergedState];
};

เย็น! ฉันจะห่อsetMergedStateด้วยuseMemo(() => setMergedState, [])เพราะตามที่เอกสารบอกว่าsetStateจะไม่เปลี่ยนระหว่างการแสดงผลซ้ำ: React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.วิธีนี้ฟังก์ชัน setState จะไม่ถูกสร้างขึ้นใหม่ในการแสดงผลซ้ำ
tonix

0

คุณยังสามารถใช้useEffectเพื่อตรวจจับการเปลี่ยนแปลงสถานะและอัปเดตค่าสถานะอื่น ๆ ตามนั้น


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