乐趣区

关于程序员:2023年了useEffect你真的会用嘛

2023 年了~useEffect 你真的会用嘛

前言

这篇文章是我依据 a -complete-guide-to-useeffect 本人总结进去的一些重点~

依照程序顺次如下:

  • React 单向数据流的渲染
  • Effect 的执行机会
  • 不要对 Effect 扯谎:依赖数组要怎么设置
  • 应用 setStateuseReducer将上报行为和状态更新解耦
  • 函数是否能够作为 Effect 的依赖呢?

同时咱们能够在浏览的时候时刻问本人以下问题:

  • 🤔 如何用 useEffect 模仿 componentDidMount 生命周期?
  • 🤔 如何正确地在 useEffect 里申请数据?[]又是什么?
  • 🤔 我应该把函数当做 effect 的依赖吗?
  • 🤔 为什么有时候会呈现有限反复申请的问题?
  • 🤔 为什么有时候在 effect 里拿到的是旧的 state 或 prop?

每一次渲染都有它本人的 props 和 state

function Counter() {const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
+      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上述例子中,对于 <p>You clicked {count} times</p> 中的count,可能 react 的初学者会认为其是响应式的,也就是会主动监听并且更新渲染。

但实际上,它只是一个一般的变量 count,没有任何的data bounding。咱们在每一次调用setCount 的时候,实际上是 React 应用新的 count 值来重复渲染组件。第一次是 0,第二次是 1,以此类推。。。

除了变量,那么事件处理函数呢?

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

  function handleAlertClick() {setTimeout(() => {alert('You clicked on:' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

咱们设想一下:

  • 首先间断点击 Click me 两次
  • 再点击一次 Show alert
  • 持续点击 Click me

在点击 alert 的 3 秒过后,其显示的值会是什么呢?

答案是 2!!,这也证实了咱们上述的论断。即事件处理函数跟变量一样,也是独立属于以后的渲染。

每次渲染都有它独立的 Effect

咱们来看看官网上的例子:

function Counter() {const [count, setCount] = useState(0);
  useEffect(() => {document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

这里依据之前的学习咱们晓得,并不是「不变的 Effect 中 count 的值发现了变动,而是 Effect 每一次渲染都不雷同,每一次 Effect 都捕捉了以后渲染的 count

<div style=”color:#d2568c;border-left:5px solid;padding:6px”><span style=”font-weight:bold;”>「React 会记住你提供的 effect 函数,并且会在每次更改作用于 DOM 并让浏览器绘制屏幕后去调用它。」</span></div>

useEffect

每一次渲染都有它本人的 …… 所有

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

  useEffect(() => {setTimeout(() => {console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

咱们来思考一下上述代码,咱们给每一次渲染都增加一个延时函数。当咱们疯狂点击 Click me 的时候,会产生什么呢?

答案是:「程序打印 0,1,2,3…」

当然,你可能会想,如果我就是想要获取最新的值要怎么办的?应用useRef

function Example() {const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  // ...

此时当咱们疯狂点击后,最终打印进去的全都是 最新的 count

Effect 中的清理又是怎么样的?

思考官网的案例:

useEffect(() => {ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });

假如第一次渲染的时候 props 是{id: 10},第二次渲染的时候是{id: 20}。你可能会认为产生了上面的这些事:

  • React 革除了 {id: 10} 的 effect。
  • React 渲染 {id: 20} 的 UI。
  • React 运行 {id: 20} 的 effect。

事实上并不是这样的。React 只会在浏览器渲染结束后才会去执行 Effect,这并不会阻塞渲染使得你的利用更加晦涩。

Effect 的清理实际上也被延后了。上一次 Effecet 的清理会在 UI 渲染完结后被执行。

  • React 渲染 {id: 20} 的 UI。
  • 浏览器绘制 咱们在屏幕上看到 {id: 20} 的 UI。
  • React 革除 {id: 10} 的 effect。
  • React 运行 {id: 20} 的 effect。

通知 react 如何去比对 useEffect

简略来说,你的 Effect 可能会因为其余状态变动了而造成了不必要的调用。
为了解决这个问题,能够通过 依赖数组 来通知 React

如果以后渲染中的这些依赖项和上一次运行这个 effect 的时候值一样,因为没有什么须要同步,React 会主动跳过这次 effect。

如果依赖项设置谬误会怎么样呢?

举个例子,咱们来写一个每秒递增的计数器。在 Class 组件中,咱们的直觉是:“开启一次定时器,革除也是一次”

当咱们天经地义地把它用 useEffect 的形式翻译,直觉上咱们会设置依赖为[]。因为“我只想运行一次 effect“。

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

  useEffect(() => {const id = setInterval(() => {setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

在这个例子中,·count的值永远都不会超过 1 因为定时器只会设置一次,而此时的 count 永远都是 0,即永远都在执行setCount(0+1)

咱们必须扭转咱们的想法。所谓 「依赖数组」 是咱们用来通知 React 该 Effect 用到了什么状态。在这个例子中,咱们对 React 扯谎了,通知 React 说没有用到任何组件内的值。但实际上依赖了count

要诚恳的通知 React 咱们正确的依赖的两种方法

1. 将所有 Effect 用到的依赖都通知给 React

  useEffect(() => {const id = setInterval(() => {setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
+  }, [count]);

当初依赖正确了,然而会有一个问题,就是每一次执行 setCount 的时候,都会导致计数器的革除和新建。这并不合乎咱们的初衷。

2. 批改 effect 外部的代码以确保它蕴含的值只会在须要的时候产生变更。

咱们不想告知谬误的依赖 – 咱们只是批改 effect 使得依赖更少。

持续看会下面的示例(将 count 传入依赖数组)。咱们须要思考,咱们为什么须要用到 count?咱们想要通过setCount 来更新 count 的值。

但其实在这个场景,咱们并不需要获取 count 的值,咱们只须要获取上一次的状态进行解决即可。因而能够写成

useEffect(() => {const id = setInterval(() => {setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

函数式更新

对于 Effect 来说,咱们能够用 「同步」 的思维去了解。

以下面这个例子来说,同步的一个乏味的中央就是将同步信息和状态解耦。相似于 setCount(c => c + 1) 这样的更新模式比 setCount(count + 1) 传递了更少的信息,因为它不再被以后的 count 值“净化”。它只是表白了一种行为(“递增”)。

然而,setCount(c => c + 1)并不完满,例如咱们有两个状态相互依赖,即下一次状态不仅仅依赖上一次状态,它就不能做到了。

这时候,useReducer大哥就要出场了。

useReducer

咱们对下面例子做一些批改:

function Counter() {const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {const id = setInterval(() => {setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

当初 count 的值不仅仅依赖本人上一次的值,还依赖 step 状态。

雷同的,目前咱们并没有对 React 说谎,因而它能够正确运行。但 问题在于 每一次扭转 step 后,计时器都会被销毁重建。

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值 时,你可能须要用 useReducer 去替换它们。

当你写相似 setSomething(something => ...) 这种代码的时候,兴许就是思考应用 reducer 的契机。reducer能够让你把组件内产生了什么 (actions) 和状态如何响应并更新离开表述。

function Counter() {const [state, dispatch] = useReducer(reducer, initialState);
  const {count, step} = state;

  useEffect(() => {const id = setInterval(() => {dispatch({ type: 'tick'}); // 更新 count
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
      // 更新 step
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

function reducer(state, action) {const { count, step} = state;
  if (action.type === 'tick') {return { count: count + step, step};
  } else if (action.type === 'step') {return { count, step: action.step};
  } else {throw new Error();
  }
}

应用 useReducer 能够完满解决上述问题。这是为什么呢?

答案在于 <span style=”color:#d2568c;font-weight:bold;”>「React 会保障 dispatch 在组件的申明周期内放弃不变」</span>。所以下面例子中不再须要从新订阅定时器。

(你能够从依赖中去除 dispatch, setState, 和useRef 包裹的值因为 React 会确保它们是动态的。不过你设置了它们作为依赖也没什么问题。)

与其在 Effect 中去获取状态,不如只是 dispatch 一个 action 来形容行为,这使得 Effect 与状态解耦,Effect 再也不必关怀具体的状态了~

应用 useRuducer 真的是在舞弊

当咱们的 step 是 props 传进来的又会怎么呢?咱们还是能够应用 useReducer,但这种状况下咱们的reducer 函数就得写进组件中,因为要获取 props。

function Counter({step}) {const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {if (action.type === 'tick') {return state + step;} else {throw new Error();
    }
  }

  useEffect(() => {const id = setInterval(() => {dispatch({ type: 'tick'});
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

有的小伙伴可能会问:上一次渲染的 reducer 怎么能获取到最新的 props 呢?

答案是:Effect 只是 dispatch 了行为 – 它会在下一次渲染中去调用 reducer,这是就能够获取到最新的 props 了。留神:「reducer 不是在 Effect 中调用的」

永远要记住 useEffect 的执行机会

useEffect 的执行机会在 Dom 渲染结束后。而且它是异步执行的,不会阻塞的。因而在上面例子中,会呈现页面闪动的成果。(0 -> 12 -> 2)


export default function FuncCom () {const [counter, setCounter] = useState(0);

    useEffect(() => {if (counter === 12) {
            // 为了演示,这里同步设置一个延时函数 500ms
            delay()
            setCounter(2)
        }
    });
    return (
        <div style={{fontSize: '100px'}}>
            <div onClick={() => setCounter(12)}>{counter}</div>
        </div>
    )
}

换成了 useLayoutEffect 后,屏幕上只会呈现 0 和 2,这是因为 useLayoutEffect 同步个性 ,会在 浏览器渲染之前同步更新 react DOM 数据,哪怕是屡次的操作,也会在渲染前一次性解决完,再交给浏览器绘制。这样不会导致闪屏景象产生。

将函数放进 Effect 中

首先抛出问题:函数真的不应该成为 依赖项嘛?

咱们来看一个许多程序员都会写的一个案例:

function SearchResults() {const [data, setData] = useState({hits: [] });

  async function fetchData() {
    const result = await axios('https://hn.algolia.com/api/v1/search?query=react',);
    setData(result.data);
  }

  useEffect(() => {fetchData();
  }, []); // Is this okay?

  // ...
}

该代码是能够失常运行的,然而它的拓展性十分差。试想一下,在我的项目代码一直增长下,咱们可能会拆散出很多个很长的函数,可能会有其余的依赖:

function SearchResults() {const [query, setQuery] = useState('react');

  // Imagine this function is also long
  function getFetchUrl() {return 'https://hn.algolia.com/api/v1/search?query=' + query;}

  // Imagine this function is also long
  async function fetchData() {const result = await axios(getFetchUrl());
    setData(result.data);
  }

  useEffect(() => {fetchData();
  }, []);

  // ...
}

如果咱们遗记更新 Effect 的依赖数组的话,这不仅仅坑骗了 React,而且导致 Effect 不会同步所依赖的 state 和 props。

一个最简略的解决方案是,如果某些函数仅在 effect 中调用,你能够把它们的定义移到 effect 中:。这样的话程序员不须要思考这些“间接依赖”,而且在增加 query 状态的时候,意识到 Effect 依赖于它。

function SearchResults() {const [query, setQuery] = useState('react');

  useEffect(() => {
  // ❇️ get together
    function getFetchUrl() {return 'https://hn.algolia.com/api/v1/search?query=' + query;}

    async function fetchData() {const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();}, [query]); // ✅ Deps are OK

  // ...
}

咱们必须意识到:·useEffect设计用意就是要强制你去关注数据流的变动,而后决定怎么去让 Effect 同步!!

但我不能将函数放进 Effect 中

举个例子,比方某函数被多个 Effect 应用到了。

首先咱们要明确一点,在组件中定义的函数每一次渲染都是变动的,但这个事实也给咱们带来了问题。比方:

function SearchResults() {function getFetchUrl(query) {return 'https://hn.algolia.com/api/v1/search?query=' + query;}

  useEffect(() => {const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  useEffect(() => {const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  // ...
}

一方面咱们不想在每一个 Effect 中都复制雷同的代码,但这又是对 React 的不诚实~,另一方面假如咱们将 getFetchUrl 当作依赖项,但它每一次渲染都在变,咱们的依赖数组就没有施展用途了。

这里有两个解决办法:

  1. 如果函数并没有用到组件内的值,咱们能够大胆地将其移到组件内部!
  2. 应用useCallBackhook 包裹。

useCallBack实质上只是加了一层依赖查看,使得函数自身只有在须要时才扭转,这样咱们也不须要去掉函数依赖。

当函数用到了组件内的值,比方 query 能够通过输入框由用户去扭转,咱们能够这样做:

function SearchResults() {const [query, setQuery] = useState('');
  const getFetchUrl = useCallback(() => {return 'https://hn.algolia.com/api/v1/search?query=' + query;}, [query]); // ✅ Deps are OK

  useEffect(() => {const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Deps are OK

  useEffect(() => {const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Deps are OK

  // ...
}

这才是拥抱 React 数据流和同步思维的最终后果。query扭转 -> getFetchUrl扭转 -> Effect 从新执行。反之,如果 query 不变,则 Effect 将不会从新执行

函数是数据流的一部分吗?

乏味的是,这种模式在 class 组件中是行不通的。

class Parent extends Component {
  state = {query: 'react'};
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {data: null};
  componentDidMount() {this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    // 🔴 This condition will never be true
    if (this.props.fetchData !== prevProps.fetchData) {this.props.fetchData();
    }
  }
  render() {// ...}
}

在 class 组件中,类办法 this.fetchData 是永远不会变的

为了解决这个问题,咱们必须将 query 参数传给 Child 组件即便它并没有间接应用到query

class Child extends Component {componentDidUpdate(prevProps) {
    // 🔴 This condition will never be true
    if (this.props.query !== prevProps.query) {this.props.fetchData();
    }
}

在 class 组件中,函数属性自身并不是数据流的一部分。组件的办法中蕴含了可变的 this 变量导致咱们不能确定无疑地认为它是不变的。因而,即便咱们只须要一个函数,咱们也必须把一堆数据传递上来仅仅是为了做“diff”。咱们无奈晓得传入的this.props.fetchData 是否依赖状态,并且不晓得它依赖的状态是否扭转了。

然而感激useCallback,它让函数自身也参加进了 React 数据流,咱们能够说函数的输出变了,那么函数自身就变了。(函数的输出须要咱们来定义)

🌟须要强调的是 :当咱们须要将函数传递给子组件并且在子组件的 Effect 中使用的话,最好应用useCallback 将其包裹。

对于竞态

上面是一个 class 组件发申请的案例

class Article extends Component {
  state = {article: null};
  componentDidMount() {this.fetchData(this.props.id);
  }
  async fetchData(id) {const article = await API.fetchArticle(id);
    this.setState({article});
  }
  // ...
}

心细的小伙伴可能发现了,下面代码并没有思考到 id 更新的状况,于是:

 componentDidUpdate(prevProps) {if (prevProps.id !== this.props.id) {this.fetchData(this.props.id);
    }
  }

然而这真的能够完满解决所有问题嘛?

咱们能够试想一下,比方我先申请 {id: 10},而后更新到 {id: 20},但{id: 20} 的申请更先返回。申请更早但返回更晚的状况会谬误地笼罩状态值。这就是「竞态」

最好的权宜之计就是利用一个 boolean 来去记录以后申请状态。

更多相干常识能够看这篇文章

结尾

我是 <span style=”color:#87481f;font-weight:bold”>「盐焗乳鸽还要香锅」</span>,喜爱我的文章欢送关注噢

  • github 链接 https://github.com/1360151219
  • 博客链接是 strk2.cn
  • 掘金账号、知乎账号、简书《盐焗乳鸽还要香锅》
  • 思否账号《天天摸鱼真的爽》

本文由 mdnice 多平台公布

退出移动版