簡體   English   中英

使用 Hooks 實現 React 父子組件的最佳實踐

[英]Best Practice to Implement React Parent-Child Components Using Hooks

我正在學習 React,但不確定我是否正確地這樣做了。 為了提出這個問題,我已經閱讀了所有關於 React hooks 的內容; 我孤立地理解它們,但在現實生活中很難將它們拼湊在一起。

想象一下,我有一個父組件,其中包含通過父級上的map函數生成的子組件列表:

<Parent>
  {items.map(i => <Child item={i} />)}
</Parent>

並說 Child 組件只是一個簡單的:

function Child({item}) {
  return <div>{item}</div>
}

但是,子組件需要更新其視圖,並能夠刪除自身。 我的問題是 - 我應該在孩子身上調用useState(item)以便它在內部擁有該項目的副本嗎? 在那種情況下,如果我更新了item ,父項中的items列表就不會得到更新嗎? 為了解決這個問題,我最終得到了類似的東西:

<Parent>
  {items.map(i => 
    <Child 
      item={i} 
      updateItem={(index) => setItems( /* slice and concat items list at index */ )}
      deleteItem={(index) => setItems( /* slice items list at index */ )}
    />)
  }
</Parent>

並且Child組件只是根據需要調用updateItemdeleteItem ,而不使用任何 React 掛鈎。

我的問題如下:

  1. 我應該在子組件中使用useState嗎?
  2. 我應該以某種方式在updateItem / deleteItem函數上使用useCallback嗎? 我嘗試使用它,但它的行為不正確(Parent 中的正確項目已被刪除,但剩余呈現的Child中的狀態顯示已刪除的Child的值。
  3. 我的理解是,這將是非常低效的,因為對 1 個孩子的更新將迫使所有其他孩子重新渲染,盡管他們沒有更新。

如果最正確和最有效地完成,代碼應該是什么樣的?

謝謝指點。

@Giorgi Moniava 的回答非常好。 我認為您也可以不用useCallback並且仍然堅持最佳實踐。

 const {useEffect, useState} = React; const Child = ({ item, update }) => { const [rerender, setRerender] = useState(0); useEffect(() => setRerender(rerender + 1), [item]); useEffect(() => setRerender(rerender + 1), []); return ( <div className="row"> <div className="col">{item.id}</div> <div className="col">{item.name}</div> <div className="col">{item.value}</div> <div className="col">{rerender}</div> <div className="col"> <button onClick={update}>Update</button> </div> </div> ); } const Parent = () => { const [items, setItems] = useState([ { id: 1, name: "Item 1", value: "F17XKWgT" }, { id: 2, name: "Item 2", value: "EF82t5Gh" } ]); const add = () => { let lastItem = items[items.length - 1]; setItems([...items, { id: lastItem.id + 1, name: "Item " + (lastItem.id + 1), value: makeid(8) } ]); }; const update = (sentItem) => { setItems( items.map((item) => { if (item.id === sentItem.id) { return {...item, value: makeid(8) }; } return item; }) ); }; const makeid = (length) => { var result = ""; var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var charactersLength = characters.length; for (var i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; }; return ( <div className="parent"> <div className="header"> <h1>Parent Component</h1> <h2>Items in Parent State</h2> </div> <div className="table"> <section> <header> <div className="col">ID</div> <div className="col">NAME</div> <div className="col">VALUE</div> </header> {items.map((item, i) => ( <div className="row" key={item + "-" + i}> <div className="col">{item.id}</div> <div className="col">{item.name}</div> <div className="col">{item.value}</div> </div> ))} </section> <div className="header"> <h1>Children</h1> <h2>Based on Items state</h2> </div> <button onClick={add}>Add</button> <section> <header> <div className="col">ID</div> <div className="col">Name</div> <div className="col">Value</div> <div className="col">Re-render</div> <div className="col">Update</div> </header> {items.map((item, i) => ( <Child item={item} key={"child-" + item + "-" + i} update={() => update(item)} /> ))} </section> </div> </div> ); } ReactDOM.render( <Parent />, document.getElementById("root") );
 .parent { font-family: sans-serif; }.header { text-align: center; } section { display: table; width: 100%; } section > * { display: table-row; background-color: #eaeaea; } section.col { display: table-cell; border: 1px solid #cccccc; }
 <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <div id="root"></div>

不,您不必創建內部狀態。 這是一種創建本地狀態只是為了保留組件道具副本的反模式。

在您的情況下,您可以將狀態保留在父組件上。 您的子組件可以像您使用的那樣執行回調,

例如,

const [items, _] = useState(initialItemArray);

const updateItem = useCallback((updatedItem) => {
  // do update
}, [items])

const deleteItem = useCallback((item) => {
 // do delete
}, [items])

<Child
  data={item}
  onUpdate={updateItem}
  onDelete={deleteItem}
/>

另請注意,您不應過度使用useCallbackuseMemo 例如,如果您的列表太大並且您對子項使用 useMemo & React 重新呈現多個 100 - 1000 個列表項,這可能會導致性能問題,因為 React 現在必須在memo hoc 中做一些額外的工作來決定您的<Child />是否應該重新渲染。 但是如果 Child 組件包含一些復雜的 UI(圖像、視頻和其他復雜的 UI 樹),那么使用 memo 可能是更好的選擇。


要解決第三點中的問題,您可以為每個子組件添加一些唯一的鍵 ID。

<Child
  key={item.id} // assuming item.id is unique for each item
  data={item}
  onUpdate={(updatedItem) => {}}
  onDelete={(item) => {}}
/>

現在 React 足夠聰明,不會因為你更新一個或刪除一個而重新渲染整個列表。 這就是為什么你不應該使用數組索引作為 key prop 的原因之一

我應該在子組件中使用 useState 嗎?

通常復制狀態不是一個好主意; 所以可能沒有。

我應該以某種方式在 updateItem/deleteItem 函數上使用 useCallback

如果你想將這些回調傳遞給包裝在React.memo中的組件,你可能需要它。

我的理解是,這將是非常低效的,因為對 1 個孩子的更新將迫使所有其他孩子重新渲染,盡管他們沒有更新

是的,你的理解是正確的,但你是否會注意到速度變慢,取決於很多事情,比如有多少子組件,它們各自渲染什么,等等。

如果最正確和最有效地完成,代碼應該是什么樣的?

見下文。 請注意,我添加了React.memo ,它與useCallback一起應該可以防止這些項目重新渲染,其道具沒有改變。

const Child = React.memo(function MyComponent({ item, update }) {
  console.log('Rendered', item);
  return (
    <div
      onClick={() => {
        update(item);
      }}
    >
      {item.name}
    </div>
  );
});

let itemsData = [
  { id: 0, name: 'item1' },
  { id: 1, name: 'item2' },
];
export default function App() {
  let [items, setItems] = React.useState(itemsData);
  let update = React.useCallback(
    (item) =>
      setItems((ps) =>
        ps.map((x) => (x.id === item.id ? { ...x, name: 'updated' } : x))
      ),
    []
  );
  return (
    <div>
      {items.map((item) => (
        <Child key={item.id} item={item} update={update} />
      ))}
    </div>
  );
}

現在,如果您單擊item1 ,將不會調用item2console.log

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM