热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

React useEffect使用教程

React useEffect使用教程-目录一、每一次渲染都有它自己的PropsandState二、每次渲染都有它自己的Effects三、关于依赖项不要对React撒谎四、两种诚实

这篇文章会假设你对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 (
    

You clicked {count} times

); }

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 {count};
}

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

在第一次渲染中我们执行了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 (
    <>
      {count}
       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 (
    <>
      {count}
       {
        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里修改了一个单元格的值,另一个使用它的单元格会自动重新计算一样。


推荐阅读
  • 我正在尝试使用scrapycrallsingle运行完美运行的scrapy蜘蛛,但我无法在python脚本中运行它.主要问题是从不执行SingleBlogSpider.parse方 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 开发笔记:图像识别基于主成分分析算法实现人脸二维码识别
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了图像识别基于主成分分析算法实现人脸二维码识别相关的知识,希望对你有一定的参考价值。 ... [详细]
  • React提供三种方式创建Refs:字符串Refs(将被废弃)回调函数RefsReact.createRef(从React16.3开始)第一种方式不推荐使用,原因在此,并且可能会在之后的版本移除。classMyComponentextendsReact.Component{constructor(props){sup ... [详细]
  • Error in nextTick: quot;TypeError: Cannot set property #39;xxx#39; of undefinedquot;解决办法
    vue项目在控制台中报这个错误时,当看到nextTick词时想到vue的$nextTick()方法Vue在更新DOM时是异步执行的。只要侦听到数据变化, ... [详细]
  • Arduino + ESP32C3 + TFT(1.8‘ ST7735S)基础平台(实验四)直接显示网络图片
    ------------------------------------------------------------------------------------------ ... [详细]
author-avatar
昕谊凡
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有