React useEffect使用教程

目录
  • 一、每一次渲染都有它自己的 Props and State
  • 二、每次渲染都有它自己的Effects
  • 三、关于依赖项不要对React撒谎
  • 四、两种诚实告知依赖的方法
  • 五、来自useReducer的助攻
  • 六、把函数移到Effects里
  • 七、我不想把可复用的函数放到Effect里

这篇文章会假设你对useEffectAPI有一定程度的了解。

一、每一次渲染都有它自己的 Props and State

在我们讨论 effects 之前,我们需要先讨论一下渲染,当我们更新 state 的时候,React会重新渲染组件。每一次渲染都能拿到独立的 state,这个状态值是函数中的一个常量。

这里关键的点在于任意一次渲染中的常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的值独立于其他渲染。

如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的值。

二、每次渲染都有它自己的Effects

让我们先看向官网的 useEffect 的例子:

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状态值的呢?

也许,是某种 watching 机制类似 vue 中的数据响应式使得能够在 effect 函数内更新?也或许是一个可变的值,React 会在我们组件内部修改它以使我们的 effect 函数总能拿到最新的值?

都不是。

我们已经知道是某个特定渲染中的常量。事件处理函数“看到”的是属于它那次特定渲染中的状态值。对于 effects 也同样如此:

并不是count的值在“不变”的 effect 中发生了改变,而是 effect 函数本身在每一次渲染中都不相同。

React 会记住你提供的 effect 函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。

所以虽然我们说的是一个 effect(这里指更新document的title),但其实每次渲染都是一个不同的函数— 并且每个 effect 函数看到的 props 和 state 都来自于它属于的那次特定渲染。

三、关于依赖项不要对React撒谎

现在只需要记住:如果你设置了依赖项,effect 中用到的所有组件内的值都要包含在依赖中。这包括props,state,函数组件内的任何东西。

在下面这个组件中,我们的直觉是:“开启一次定时器,清除也是一次”。直觉上我们会设置依赖为 '[]'。“我只想运行一次 effect ”,但是这样对吗?

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>{count}</h1>;
}

我们以为他会一直递增下去,但实际上他只会递增一次,你想要触发一次因为它是定时器 ,但为什么会有问题?

在第一次渲染中我们执行了setCount(0 + 1)。但是我们设置了[]依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1)。我们对 React 撒谎说我们的 effect 不依赖组件内的任何值,可实际上我们的 effect 有依赖。

四、两种诚实告知依赖的方法

第一种策略是在依赖中包含所有 effect 中用到的组件内的值。让我们在依赖中包含:count

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

这在我们大部分初级开发者的眼中都没有什么问题,并且程序确实不会出任何 bug,现在,每次修改都会重新运行 effect,这能解决问题但是我们的定时器会在每一次改变后清除和重新设定。这肯定不是我们想要的结果。

第二种策略是修改 effect 内部的代码以确保它包含的值只会在需要的时候发生变更。

在这个场景中,我们其实并不需要在effect中使用 count。当我们想要根据前一个状态更新状态的时候,我们可以使用的函数形式:

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

我们需要告知React的仅仅是去递增状态 - 不管它现在具体是什么值。注意我们做到了移除依赖,并且没有撒谎。我们的 effect 不再读取渲染中的count值。

五、来自useReducer的助攻

如果我们有两个互相依赖的状态,或者我们想基于一个 prop 来计算下一次的 state,setCount(c => c + 1)它并不能做到。幸运的是,有一个更强大的姐妹模式,它的名字叫useReducer。

我们先来修改上面的例子让它包含两个状态:count 和 step 。我们的定时器会每次在 count 上增加一个 step 值:

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))} />
    </>
  );
}

注意我们没有撒谎。既然我们在 effect 里使用了step,我们就把它加到依赖里。所以这也是为什么代码能运行正确。

这个例子目前的行为是修改会重启定时器 - 因为它是依赖项之一。在大多数场景下,这正是你所需要的。清除上一次的effect然后重新运行新的effect并没有任何错。不过,假如我们不想在改变后重启定时器,我们该如何从effect中移除对的依赖呢?

下面这句话我希望你作为一名 react 开发人员要记下来:

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

reducer 可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

我们用一个dispatch依赖去替换 effect 的依赖 step

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}
const initialState = {
  count: 0,
  step: 1,
};
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();
  }
}

你可能会问:“这怎么就更好了?”答案是React会保证dispatch在组件的声明周期内保持不变。所以上面例子中不再需要重新订阅定时器。

相比于直接在 effect 里面读取状态,它 dispatch 了一个action来描述发生了什么。这使得我们的 effect 和状态解耦。我们的 effect 不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由 reducer 去统一处理。

六、把函数移到Effects里

一个典型的误解是认为函数不应该成为依赖。举个例子,下面的代码看上去可以运行正常:

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();
  }, []);
  // ...

需要明确的是,上面的代码可以正常工作。但这样做在组件日渐复杂的迭代过程中我们很难确保它在各种情况下还能正常运行。

如果我们在某些函数内使用了某些 state 或者 prop:

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();
  }, []);
  // ...
}

如果我们忘记去更新使用这些函数(很可能通过其他函数调用)的effects的依赖,我们的effects就不会同步props和state带来的变更。这当然不是我们想要的。

如果某些函数仅在effect中调用,你可以把它们的定义移到effect中:

function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside!
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }
    fetchData();
  }, []);
}

这么做有什么好处呢?我们不再需要去考虑这些“间接依赖”。我们的依赖数组也不再撒谎:在我们的 effect 中确实没有再使用组件范围内的任何东西。

如果我们后面修改getFetchUrl去使用状态 query,我们更可能会意识到我们正在effect里面编辑它因此,我们需要把 query添加到effect的依赖里:

function SearchResults() {
  const [query, setQuery] = useState('react');
  useEffect(() => {
    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]);
}

七、我不想把可复用的函数放到Effect里

有时候你可能不想把函数移入 effect 里。比如,组件内有几个 effect 使用了相同的函数,你不想在每个 effect 里复制黏贴一遍这个逻辑。也或许这个函数是一个 prop。

在这种情况下你应该忽略对函数的依赖吗?这么做是不对的。再次强调,effects不应该对它的依赖撒谎。通常我们还有更好的解决办法。一个常见的误解是,“函数从来不会改变”。但是这篇文章你读到现在,你知道这显然不是事实。实际上,在组件内定义的函数每一次渲染都在变。

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 ...
  }, []);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []);
}

在这个例子中,你可能不想把getFetchUrl移到 effects 中,因为你想复用逻辑。

另一方面,如果你对依赖很“诚实”,你可能会掉到陷阱里。我们的两个 effects 都依赖 getFetchUrl,而它每次渲染都不同,所以我们的依赖数组会变得无用:

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 ...
  }, [getFetchUrl]);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

我们有两个更简单的解决办法。

第一个, 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在 effects 中使用:

function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []);
  // ...
}

你不再需要把它设为依赖,因为它们不在渲染范围内,因此不会被数据流影响。

或者, 你也可以把它包装成useCallback Hook:

function SearchResults() {
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

我们用 useCallback 对 getFetchUrl 做了一层缓存,现在只有当依赖项变化的时候,才会重新执行 useCallback 来返回新的函数,依赖项没有变化的时候就算组件 rerender 了,这个函数也不会重新执行,这样我们把 getFetchUrl 作为 useEffect 的依赖就没问题了。

不同于传递参数的方式,现在我们从状态中读取 query:

function SearchResults() {
  const [query, setQuery] = useState('react');
  const getFetchUrl = useCallback(() => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, [query]);
  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]);
  // ...
}

如果query保持不变,useCallback也会保持不变,我们的 effect 也不会重新运行。但是如果修改了 query,useCallback 也会随之改变,因此会重新请求数据。这就像你在Excel里修改了一个单元格的值,另一个使用它的单元格会自动重新计算一样。

到此这篇关于React useEffect使用教程的文章就介绍到这了,更多相关React useEffect内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈react useEffect闭包的坑

    问题代码 看一段因为useEffect导致的闭包问题代码 const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('clic

  • react中使用useEffect及踩坑记录

    目录 使用useEffect及踩坑记录 useEffect 介绍 useEffect常见跳坑 hooks中useEffect()使用总结 常见使用 useEffect() 的第二个参数说明 useEffect() 第一个函数参数的返回值 useEffect() 的注意点 使用useEffect及踩坑记录 useEffect 介绍 useEffect时reactHook中最重要,最常用的hook之一. useEffect相当于react中的什么生命周期呢? 这个问题在react官网中有过介绍,在使

  • 关于 React 中 useEffect 使用问题浅谈

    目录 前言 优化前 优化后 总结 前言 最近看了一下 ant-design 中的 tree 组件源码时发现 useEffect 中根据 props 来计算当前函数组件的 state 的,感到好奇,因为这样会导致应用重新绘制一次,这样才复杂场景下会对应用有一定的性能影响.为了验证自己猜想是否正确做了一下实践.这里的 React 是官方 16.12.0的源码. 优化前 import * as React from './react-source/packages/react' import * as

  • React中useEffect 与 useLayoutEffect的区别

    目录 前置知识 useEffect commitBeforeMutationEffects commitMutationEffects commitLayoutEffects 后续阶段 useLayoutEffect 结论 前置知识 我们可以将 React 的工作流程划分为几大块: render 阶段:主要生成 Fiber节点 并构建出完整的 Fiber树 commit 阶段:在上一个render 阶段中会在 rootFiber 上生成一条副作用链表,应用的DOM操作就会在本阶段执行 commi

  • React深入分析useEffect源码

    目录 热身准备 初始化 mount 更新 update updateEffect 执行副作用 总结 热身准备 这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是: 执行时机不同: useEffect是异步, useLayoutEffect是同步,会阻塞渲染: 初始化 mount mountEffect 在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法 var hook =

  • React中useEffect与生命周期钩子函数的对应关系说明

    目录 React useEffect与生命周期钩子函数的对应关系 使用格式一:不带参数的情况 使用格式二:带第二个参数,参数为空数组 使用格式三:带第二个参数,并且指定了依赖项 使用格式四:依赖项为空,没有具体的副作用函数.但是有副作用函数的清理函数. React函数式组件用useEffect模拟三个生命周期钩子函数 React useEffect与生命周期钩子函数的对应关系 在React的函数组件中,useEffect的作用其实也对标了类组件中的生命周期,它的四种使用格式也与生命周期的四种钩子

  • React useEffect使用教程

    目录 一.每一次渲染都有它自己的 Props and State 二.每次渲染都有它自己的Effects 三.关于依赖项不要对React撒谎 四.两种诚实告知依赖的方法 五.来自useReducer的助攻 六.把函数移到Effects里 七.我不想把可复用的函数放到Effect里 这篇文章会假设你对useEffectAPI有一定程度的了解. 一.每一次渲染都有它自己的 Props and State 在我们讨论 effects 之前,我们需要先讨论一下渲染,当我们更新 state 的时候,Rea

  • React useEffect的理解与使用

    React16.8新增的useEffec这个hook函数就是处理副作用的. 所谓的"副作用",举个通俗一点的例子,假如感冒了本来吃点药就没事了,但是吃了药发现身体过敏了,而这个"过敏"就是副作用. 放到React中,本来只是想渲染DOM展示到页面上,但除了DOM之外还有数据,而这些数据必须从外部的数据源中获取,这个"获取外部数据源"的过程就是副作用. useEffect怎么用可以参考官网给出的例子,这里主要针对使用useEffect过程中遇到的问

  • 如何解决React useEffect钩子带来的无限循环问题

    目录 什么导致的无限循环以及如何解决它们 如何解决这个问题 使用函数作为依赖项 使用数组作为依赖项 将对象作为依赖项传递 传递不正确的依赖项 结尾 React的useEffect Hook可以让用户处理应用程序的副作用.例如: 从网络获取数据:应用程序通常在第一次加载时获取并填充数据.这可以通过useEffect函数实现 操作UI:应用程序应该响应按钮点击事件(例如,打开一个菜单) 设置或结束计时器:如果某个变量达到预定义值,则内置计时器应自行停止或启动 尽管useEffect Hook在Rea

  • React useEffect异步操作常见问题小结

    目录 三个常见的问题: 一.react hooks发异步请求 二.如何在组件加载的时候发起异步任务 三.如果在响应回来之前组件被销毁了会怎样? 四.如何在组件交互时发起异步任务 为什么两种写法会有差异呢? 五.其他陷阱 总结 useEffect 和异步任务搭配使用的时候会遇到的一些坑总结. 三个常见的问题: 1.如何在组件加载的时候发起异步任务 2.如何在组件交互的时候发起异步任务 3.其他陷阱 一.react hooks发异步请求 1.使用useEffect发起异步任务,第二个参数使用空数组可

  • React useEffect不支持async function示例分析

    目录 引言 React为什么这么设计呢? 简单改造 1.简单改造的写法(不推荐) 2.把异步提取成单独函数或自定义hook(推荐) 引言 useEffect相比大家都耳熟能详啦,如下这种写法,应该是非常常见的需求. useEffect(async () => { await getPoiInfo(); // 请求数据 }, []); 但是 React 本身并不支持这么做,理由是 effect function 应该返回一个销毁函数(effect:是指return返回的cleanup函数),如果

  • vue转react useEffect的全过程

    目录 vue转react useEffect useEffect的第二个参数 useEffect的使用 useEffect清除 vue2转战React Hooks实践 开发思路上 代码组织结构上 vue转react useEffect useEffect用于处理组件中的effect,通常用于请求数据,事件处理,订阅等相关操作. useEffect的第二个参数 1.当useEffect没有第二个参数时 通过这个例子可以看到useEffect没有第二个参数时不停的在调用 2.当useEffect第二

  • React useEffect的理解与使用

    目录 useEffect 介绍 特殊情况处理 useEffect 介绍 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合. componentDidMount 组件挂载 componentDidUpdate 组件更新 componentWillUnmount 组件将要摧毁 使用方法 情况一: useEffect需

  • 超级给力的JavaScript的React框架入门教程

    React 是 Facebook 里一群牛 X 的码农折腾出的牛X的框架. 实现了一个虚拟 DOM,用 DOM 的方式将需要的组件秒加,用不着的秒删.React 扮演着 MVC 结构中 V 的角色, 不过你要是 Flux 搭配使用, 你就有一个很牛X的能让轻松让 M 和 V 同步的框架了,Flux 的事以后再说~ 组件们 在 React 中,你可以创建一个有特殊功能的组件,这在 HTML 元素里你是打着灯笼也找不到的,比如这个教程里的下拉导航.每个组件都有自己的地盘(scope),所以我们定义一

  • React Native学习教程之自定义NavigationBar详解

    前言 在刚开始学习React Native的时候,版本还是0.20,问题一大堆,Navigation这个问题更是很多,首先,是NavigationBar的问题,NavigationIOS有NavigationBar,Navigation却需要自定义一个,最后,我想了想,还是自定义一个view,岂不更好,现在新公司不用RN,我正好有点时间,就把自定义的NavigationBar分享给大家.好了少废话,上代码: 示例代码 // NavigationBar 导航条的自定义封装 // create by

随机推荐