web-dev-qa-db-ja.com

useEffectを使用して、最初のレンダリングでエフェクトの適用をスキップするにはどうすればよいですか?

Reactの新しいエフェクトフックを使用すると、特定の値が再レンダリング間で変更されていない場合、エフェクトの適用をスキップするようReactに指示できます-Reactのドキュメントの例:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

ただし、上記の例は、最初のレンダリングと、countが変更された後続の再レンダリングに効果を適用します。 Reactに初期レンダリングの効果をスキップするように指示するにはどうすればよいですか?

19
uturnr

ガイドが述べているように、

エフェクトフックuseEffectは、関数コンポーネントから副作用を実行する機能を追加します。 ReactクラスのcomponentDidMount、componentDidUpdate、およびcomponentWillUnmountと同じ目的を果たしますが、単一のAPIに統合されています。

このガイドの例では、countは最初のレンダリングでのみ0であると想定されています。

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

したがって、追加のチェックを使用してcomponentDidUpdateとして機能します。

useEffect(() => {
  if (count)
    document.title = `You clicked ${count} times`;
}, [count]);

これは基本的に、useEffectの代わりに使用できるカスタムフックの動作方法です。

function useDidUpdateEffect(fn, inputs) {
  const didMountRef = useRef(false);

  useEffect(() => {
    if (didMountRef.current)
      fn();
    else
      didMountRef.current = true;
  }, inputs);
}

useRefの代わりにsetStateを提案すると、クレジットは@Tholleに移動します。

40
Estus Flask

参照の代わりに通常の状態変数を使用します。

// Initializing didMount as false
const [didMount, setDidMount] = useState(false)

// Setting didMount to true upon mounting
useEffect(() => setDidMount(true), [])

// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])

DidMountロジックは、このようなカスタムフックとしてリファクタリングできます。

function useDidMount() {
  const [didMount, setDidMount] = useState(false)
  useEffect(() => setDidMount(true), [])

  return didMount
}

最後に、このようにコンポーネントで使用できます。

const didMount = useDidMount()

const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])
5
Amiratak88

以下は、現在のレンダーが最初のレンダー(コンポーネントがマウントされたとき)かどうかを示すブールフラグを提供するだけのカスタムフックです。これは他の回答のいくつかとほぼ同じですが、useEffectまたはrender関数または必要なコンポーネントの他の場所でフラグを使用できます。誰かがより良い名前を提案できるかもしれません。

import { useRef, useEffect } from 'react';

export const useIsMount = () => {
  const isMountRef = useRef(true);
  useEffect(() => {
    isMountRef.current = false;
  }, []);
  return isMountRef.current;
};

次のように使用できます。

import React, { useEffect } from 'react';

import { useIsMount } from './useIsMount';

const MyComponent = () => {
  const isMount = useIsMount();

  useEffect(() => {
    if (isMount) {
      console.log('First Render');
    } else {
      console.log('Subsequent Render');
    }
  });

  return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
};

興味のある方は以下のテストをご覧ください。

import { renderHook } from 'react-hooks-testing-library';

import { useIsMount } from '../useIsMount';

describe('useIsMount', () => {
  it('should be true on first render and false after', () => {
    const { result, rerender } = renderHook(() => useIsMount());
    expect(result.current).toEqual(true);
    rerender();
    expect(result.current).toEqual(false);
    rerender();
    expect(result.current).toEqual(false);
  });
});

最初の小道具が非表示にする必要があることを示している場合、使用例はアニメーション要素を非表示にすることでした。小道具が変更された場合、後のレンダリングで、要素をアニメーション化する必要がありました。

3
Scotty Waggoner

@estusのアプローチは問題ありませんが、ほとんどの場合、refを使用する必要なく、よりシンプルにします。

重要な点は、ほとんどの場合useEffectを使用して一部のデータ/コンポーネントをフェッチまたは計算することです。そのため、false(何もレンダリングされません)を返すか、最初のレンダリング時およびfetch/calculations(useEffect)-レンダリングコンポーネントの後にスピナーをロードできます。

function nicknameComponent({reader}) {
  const [nickname, nicknameSet] = useState(false)

  useEffect(() => {
    let res = await fetch('api/reader')
    res = await res.json()
    nicknameSet(res)
  }, [reader]);

  if (!nickname) return false
  //or show loading spinner
  //if (!nickname) return <LoadingSpinner>

  return <div>{nickname}</div>
}
1
RTW