簡體   English   中英

在 useState 掛鈎回調中使用副作用是否可以?

[英]Is it okey to use side effects in the useState hook callback?

想象一下情況:

const [value, setValue] = useState(false);

const setSomething = (val) => {
  setValue((prev) => {
    fn(); dispatch(action); // or any other side effect
    
    return prev + val;
  });
};

useState回調中調用副作用的反應原則在編程上是否可行且很好? 它會以某種方式影響渲染過程嗎?

updater 函數中使用副作用是不行的。 可能會影響渲染過程,具體取決於特定的副作用。

反應原則(關注點分離,聲明性代碼)並不好

(我記得見過一些特殊的用例,其中將一些代碼放在更新程序函數中據說是唯一的解決方案,但我不記得它是什么。我希望在評論中提供一個示例。)

1.使用副作用的后果

使用副作用是不行的,基本上與你不應該在 useEffect 之外的任何其他地方使用副作用的原因相同。

一些副作用可能會影響渲染過程,其他副作用可能會正常工作(技術上),但您應該依賴 setter 函數內部發生的事情

React保證,例如,如果你調用setState( prev => prev + 1 ) ,那么state現在會比以前多一個。

React 不保證為了實現這個目標會在幕后發生什么。 React 可能會多次調用這些 setter 函數,或者根本不調用,並且以任意順序:

StrictMode - 檢測意外的副作用

...因為上述方法可能會被多次調用,所以它們不包含副作用很重要。 ...

2. 遵循反應原則

您不應該將副作用放在更新程序函數中,因為它驗證了一些原則,例如關注點分離和編寫聲明性代碼。

關注點分離:

setCount除了設置count之外什么都不做。

編寫聲明性代碼:

通常,您應該編寫聲明式代碼,而不是命令式代碼。

  • 即你的代碼應該“描述”狀態應該是什么,而不是一個接一個地調用函數。
  • 即你應該寫“B應該是價值X,取決於A”而不是“改變A,然后改變B”

在某些情況下,React 對您的副作用一無所知,因此您需要自己注意保持一致的狀態。

有時你無法避免編寫一些命令式代碼。

useEffect可以幫助您保持狀態一致,例如允許您將某些命令式代碼與某些狀態相關聯,也就是。 “指定依賴關系”。 如果您不使用useEffect ,您仍然可以編寫工作代碼,但您只是沒有使用 react 為此目的提供的工具 您沒有按照應有的方式使用 React,並且您的代碼變得不那么可靠。

副作用問題的示例

例如,在這段代碼中,您會期望AB始終相同,但它可能會給您帶來意想不到的結果,例如B增加 2 而不是 1(例如,在 DEV 模式和嚴格模式下):

export function DoSideEffect(){
  const [ A, setA ] = useState(0);
  const [ B, setB ] = useState(0);

  return <div>
    <button onClick={ () => {
      setA( prevA => {                // <-- setA might be called multiple times, with the same value for prevA
        setB( prevB => prevB + 1 );   // <-- setB might be called multiple times, with a _different_ value for prevB
        return prevA + 1;
      } );
    } }>set count</button>
    { A } / { B }
  </div>;
}

例如,這不會在副作用之后顯示當前值,直到組件由於某些其他原因重新渲染,例如增加count

export function DoSideEffect(){
  const someValueRef = useRef(0);
  const [ count, setCount ] = useState(0);

  return <div>
    <button onClick={ () => {
      setCount( prevCount => {
        someValueRef.current = someValueRef.current + 1; // <-- some side effect
        return prevCount; // <-- value doesn't change, so react doesn't re-render
      } );
    } }>do side effect</button>

    <button onClick={ () => {
      setCount(prevCount => prevCount + 1 );
    } }>set count</button>

    <span>{ count } / {
      someValueRef.current // <-- react doesn't necessarily display the current value
    }</span>
  </div>;
}

不,不能從狀態更新函數發出副作用,它被視為純函數

  1. 對於相同的參數,函數返回值是相同的(局部靜態變量、非局部變量、可變引用參數或輸入流沒有變化),並且
  2. 函數應用程序沒有副作用(局部靜態變量、非局部變量、可變引用參數或輸入/輸出流沒有突變)。

你可能會也可能不會使用React.StrictMode組件,但它是一種幫助檢測意外副作用的方法。

檢測意外的副作用

從概念上講,React 確實分兩個階段工作:

  • 渲染階段確定需要對 DOM 等進行哪些更改。 在這個階段,React 調用render ,然后將結果與之前的渲染進行比較。
  • 提交階段是 React 應用任何更改的時候。 (在 React DOM 的情況下,這是 React 插入、更新和刪除 DOM 節點的時候。)在這個階段,React 還會調用諸如componentDidMountcomponentDidUpdate之類的生命周期。

提交階段通常非常快,但渲染可能很慢。 出於這個原因,即將到來的並發模式(默認情況下尚未啟用)將渲染工作分成幾部分,暫停和恢復工作以避免阻塞瀏覽器。 這意味着 React 可能在提交之前多次調用渲染階段生命周期,或者它可能在根本不提交的情況下調用它們(因為錯誤或更高優先級的中斷)。

渲染階段生命周期包括以下類組件方法:

  • constructor
  • componentWillMount (或UNSAFE_componentWillMount
  • componentWillReceiveProps (或UNSAFE_componentWillReceiveProps
  • componentWillUpdate (或UNSAFE_componentWillUpdate
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState更新函數(第一個參數) <--

因為上述方法可能會被多次調用,所以它們不包含副作用很重要。 忽略此規則會導致各種問題,包括內存泄漏和無效的應用程序狀態。 不幸的是,很難檢測到這些問題,因為它們通常是不確定的。

嚴格模式不能自動為您檢測副作用,但可以通過使它們更具確定性來幫助您發現它們。 這是通過有意雙重調用以下函數來完成的:

  • 類組件constructorrendershouldComponentUpdate方法
  • 類組件靜態getDerivedStateFromProps方法
  • 功能組件體
  • 狀態更新函數( setState的第一個參數) <--
  • 傳遞給useStateuseMemouseReducer的函數

從兩個突出顯示的要點中獲取關於有意雙重調用狀態更新器函數的提示,並將狀態更新器函數視為純函數。

對於您共享的代碼片段,我認為沒有理由從更新程序回調中調用函數。 他們可以/應該在回調之外被調用。

例子:

const setSomething = (val) => {
  setValue((prev) => {
    return prev + val;
  });
  fn();
  dispatch(action);
};

我不會

僅僅因為它有效並不意味着它是一個好主意。 您共享的代碼示例將起作用,但我不會這樣做。

將不相關的邏輯放在一起會使下一個必須使用此代碼的人感到困惑; 很多時候,那個“下一個人”就是:六個月后的你,因為你完成了這個功能並繼續前進,你已經忘記了所有關於這段代碼的事情。 而現在你回來發現,一些銀器已經存放在浴室的葯櫃里,一些床單在洗碗機里,所有的盤子都在一個標有“DVD”的盒子里。

我不知道您對發布的特定代碼示例有多認真,但如果它是相關的:如果您正在使用dispatch ,這意味着您已經設置了某種減速器,或者使用useReducer鈎子,或者可能使用還原。 如果這是真的,你可能應該考慮這個布爾值是否也屬於你的 Redux 存儲:

const [ value, setValue ] = useState(false)

function setSomething(val) {
  fn()
  dispatch({ ...action, val })
}

(但它可能不會,這很好!)

如果你使用的是真正的 Redux,你也會有 action-creators,這通常是放置觸發副作用的代碼的正確位置。

無論您使用哪種狀態技術,我認為您應該更願意避免將副作用代碼放入您的各個組件中。 原因是組件通常應該是可重用的,但是如果您將副作用放入組件中,該副作用對於顯示或與組件可視化的事物的交互不是必需的,那么您只會讓其他人更難調用者使用此組件。

如果副作用對於該組件的工作方式至關重要,那么更好的處理方法是直接調用setValue和副作用函數,而不是將它們包裝在一起。 畢竟,您實際上並不依賴 useState 回調來完成您的副作用。

const [ value, setValue ] = useState(false)

function setSomething(val) {
  setValue(value + val)
  fn()
  dispatch(action)
}

暫無
暫無

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

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