useEffect

1 篇文章 0 订阅
1 篇文章 0 订阅

React的useEffect的正确使用姿势

这篇文章是在阅读了Dan神的UseEffect完整使用指南之后写下的一个个人理解,这是一篇很好的文章,而且对初学者相对友好,如果感兴趣强烈建议阅读原文。

开始正题:React的useEffect副作用函数

自从React推出了函数组件的各种hooks之后赢得很多人的青睐,可是最常用的一个钩子函数UseEffect对于很多初学者并不友好,很多人都把他当作类组件的生命周期钩子函数来使用。的确,它在某种程度上可以说是生命周期钩子的替代,可是也不完全是,具体在于他们的执行时机和依赖项上。下面来看看他们的区别。

React的组件更新中: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>
  );
}
  componentDidUpdate() {
    setTimeout(() => {
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }

第一种情况:useEffect相当于每个函数自己的作用域,每当setCount一次,浏览器挂在完此刻状态的UI后会执行useEffect(因为依赖项是没写的),所以每一次打印都是拿到属于当前状态的count

第二种情况:类组件总是指向于最新的count,他不在乎你点了多少次,三秒钟之内即使你点了无数次,他还是打印最新的值

第三种情况:如果我想改造函数组件成类组件那样每一次都拿到的是最新的值,我们怎么做?最简单的方法是useRef

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

  useEffect(() => {
    //每一次执行定时器之前,给ref赋值此刻count(最新的状态)
    latestCount.current = count;
    setTimeout(() => {
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

如何解决每一次重新选然后都重复执行useEffect?使用deps!

1、React只会更新DOM真正发生改变的部分,而不是每次渲染都大动干戈。

//例如把:
  <h1 className="color">
    green
  </h1>
//更新成:
  <h1 className="color">
    red
  </h1>
//react:可以看到两个对象:
const oldProps = {className: 'color', children: 'green'};
const newProps = {className: 'color', children: 'red'};
//react会检测每一个props,只有children发生了改变需要更新DOM节点,但是类名是没有改变的,所以react只会做
document.xxx.innerText = 'red'

2、正确的使用依赖项

因为React看不到函数里面的东西,所以我们要给React一个数组,当re-render时,数组里面的东西有一个发生改变了,就要重新执行useEffect

  useEffect(() => {
    document.title = 'Hello, ' + name;
  }, [name]);

effects更新示例图

(依赖发生了变更,所以会重新运行effect。)

但是如果我们将[]设为effect的依赖,新的effect函数不会运行:

  useEffect(() => {
    document.title = 'Hello, ' + name;
  }, []); 

effects更新示例图

(依赖没有变,所以不会再次运行effect。)

经典题目
function Counter() {
  const [count, setCount] = useState(0);
  console.log('count:', count);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

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

这么写,最大的问题就在于:useEffect的deps是空数组,re-render之后对比发现deps是没有变化的,re-render就直接跳过了此次useEffect的执行,所以每一秒后都会一直执行setCount(0+1)这个操作,但由于所有东西(节点)都没变,所以只会打印 count:1 一次

解决方法:

1、用正确的deps

每一次,count都会发生变化,那么在下一次re-render挂在完DOM之后,执行useEffect时,会先执行上一次useEffect返回的函数,但这么做就相当于每一次re-render都重新定义一个循环定时器,这操作多少有点冗余,这是一种解决方案,但不够优雅!

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

2、用一些移除依赖的技巧

在我们需要移出某些依赖的时候,我们先考虑一下,为什么会用到这些依赖?这个场景下是因为我们要setCount(count + 1),所以count就成了必需品,可是我们想做的操作不过是把count基于当前的状态不断地累加而已。这里我们可以用setState的函数的方式。因为setState((prevalue)=>{}),这种方式,React是知道当前的状态(count)的,我们需要做的只是一直调用这句话。

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

3、用加强版的useState:useReducer

当需要一个状态的更新依赖于另一个状态的时候,可以考虑一下useReduccer

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

这样做,确实没毛病,可是就是每次重新输入累加的数值之后要重新清除掉定时器,不够优雅!可以尝试使用useReducer去进行替换。

const initState = {
  count: 0,
  step: 1
}

const countReducer = (state, action) => {
  const {count, step} = state
  switch (action.type) {
    case 'count':
      return {count: count + step, step}
    case 'step':
      return {count, step: action.payload}
    default :
      return new Error('错了')
  }
}

function Counter() {
  const [count, setCount] = useState(0);
  const [state, dispatch] = useReducer(countReducer, initState)
  
  console.log(state);
  
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({type: 'count'})
      return () => {
        clearInterval(id)
      }
    }, 1000)
  }, [])
  //你可以从依赖中去除dispatch, setState, 和useRef包裹的值因为React会确保它们是静态的。不过你设置了它们作为依赖也没什么问题。

  const handleInput = (e) => {
    dispatch({
      type: 'step',
      payload: Number(e.target.value)
    })
  }
  return <div>
    <h2>{state.count}</h2>
    <input type="text" placeholder={'需要递增的值'} onInput={handleInput}/>
  </div>
}

假如我们需要依赖props去计算下一个状态呢?举个例子,也许我们的API是<Counter step={1} />,我们可以把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>;
}

这里可能有个疑惑:第一次开启了循环定时器,dispatch之后,为什么之前定义的reducer函数可以知道新的props?原因是dispatch的时候,React只记住了action那个对象,然后重新执行整个Counter组件函数,重新定义reducer函数,所以里面可以访问到新的props。

希望这篇文章对你们使用useEffect的时候有帮助

参考资料:useEffect 完整指南

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KeganRay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值