Skip to content

GeekEast/complete-guide-to-react-useEffect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Case 1: miss dependency in useEffect

  • 因为没有依赖,是无法触发页面的unmountcomponentDidUpdate的, 只会在componentDidMount时运行一次
  • 同样的值传入setState多次,只有前两次会引起re-render;
  • 为什么count的值一直是0
    • 每次渲染都是一次snapshot,会有自己state的一个version
    • 因为没有依赖,所以就没有unmount的发生,所以clean不会发生
    • clean没有发生,全局就有一次setInterval,会把count闭包闭进去
    • 闭包是闭了count这个变量,而不是它的
    • 但是count这个变量永远停留在当时渲染的那个状态(Immutable)
    • 所以setInterval中读到的count永远都是0.
import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  console.log('render');

  useEffect(() => {
    console.log('componentDidUpdate')

    const id = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000);

    return () => {
      clearInterval(id); // 只会在页面退出时执行
      console.log('clean');
    }
  }, []); // 是无法触发页面的unmount和componentDidUpdate的, 只会在componentDidMount时运行一次
  return (
    <h1>
      {count}
    </h1>
  )
}

Solution 1: append dependency

  • 如果传入count,会发生什么?
    • 因为设置了countdependency, 每次count的变动都会引起unmount, re-renderuseEffect
    • 即clean上一次setInterval, 重新渲染(更新count的值), 和重新setInterval(闭包闭了新的count)
    • 因此表现起来会很正常
  • 传入useEffect的dependencies:
    • state: 如果useEffect中又会引起state的变化,将会陷入无限循环当中
    • function:
      • 如果function定义在useEffect里面,则无需添加到依赖中
      • 如果function定义在component里面,将会陷入无限循环的渲染当中, 因为function每次渲染-引用都会发生变化
      • 如果function定义在component外面,则只会渲染一次
      • 如果必须把function定义在component里面,但是又不想无限循环,则在使用useCallback或者useMemo
import React, { useState, useEffect } from 'react';
const Counter = () => {
  const [count, setCount] = useState(0);
  console.log('render');

  useEffect(() => {
    console.log('componentDidUpdate')

    const id = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000);

    return () => {
      clearInterval(id);
      console.log('clean');
    }
  }, [count]); // 每次变化将会触发unmount和componentDidUpdate

  return (
    <h1>
      {count}
    </h1>
  )
}

export default Counter;

Solution 2: remove dependency

  • Solution 1有什么问题?
    • 按照直觉,状态的更新引起页面的重新渲染我们可以理解
    • 但是状态的更新却同时引起了页面的unmountcomponentDidUpdate, 其实是没有必要的过程
    • 我们希望只有一个setInterval,但是页面依然会根据状态变化而重新熏染

Solution A: 使用setState的函数模式

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  console.log('render');

  useEffect(() => {
    console.log('componentDidUpdate')

    const id = setInterval(() => {
      setCount(count => count + 1); // 仅仅描述了一种行为,每次执行时都会自动读取当时最新count值
    }, 1000);

    return () => {
      clearInterval(id);
      console.log('clean');
    }
  }, []); // 是无法触发页面的unmount和componentDidUpdate的

  return (
    <h1>
      {count}
    </h1>
  )
}

export default Counter;

Solution B: 使用useReducer

  • 这种情况适用于: state相互之间存在依赖,需要传入多个进入useEffectdependency
  • 建议: 能把initialStatereducer都放在component外面的,就尽量放在外面
  • 因为每次渲染都会形成新的initialStatereducer, 是没有必要的
  • 你会发现这里state的变化也不会引起unmount的发生
import React, { useEffect, useReducer } from 'react';

const initialState = { count: 0, step: 1 };
const reducer = (state, action) => {
  switch (action.type) {
    case 'TICK': return { ...state, 
      count: state.count + state.step };
    case 'STEP': return { ...state, 
      count: state.count + state.step + action.payload, 
      step: state.step + action.payload }
    default: throw new Error();
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  console.log('render');
  useEffect(() => {
    console.log('componentDidUpdate')
    const id = setInterval(() => {
      dispatch({ type: 'TICK' }); // 读取上一次最新的state交给了userReducer去完成
    }, 1000);
    return () => {
      clearInterval(id);
      console.log('clean');
    }
  }, []); // 是无法触发页面的unmount和componentDidUpdate的

  return (
    <h1>
      {state.count} <br/>
      <button onClick={() => { dispatch({ type: 'STEP', payload: 1 }) }}>Step Up</button>
    </h1>
  )
}

export default Counter;

case 2: compute state based on props

Solution 1: pass props to dispatch

  • 这种情形跟case 1类似:
    • 如果不将step例如dependency, 那么不会触发unmount, setInterval全局只有个,闭包闭的step的值一直停留在最初的值上面,因为props的变化并能引起count的变化
    • 如果step列入dependency, 将会触发unmount, setInterval一直会clean重建,读取到的props也是最新的,但是有一定性能的损耗.
import React, { useEffect, useReducer } from 'react';

const initialState = { count: 0 };
const reducer = (state, action) => {
  switch (action.type) {
    case 'TICK': return { ...state, count: state.count + action.step };
    default: throw new Error();
  }
}

const Counter = (props: any) => {
  const { step } = props;
  const [state, dispatch] = useReducer(reducer, initialState);

  console.log('render');
  useEffect(() => {
    console.log('componentDidUpdate')
    const id = setInterval(() => {
      dispatch({ type: 'TICK', step }); // 读取上一次最新的state交给了userReducer去完成
    }, 1000);
    return () => {
      clearInterval(id);
      console.log('clean');
    }
  }, [step]); // react 保证dispatch在每次渲染中都是一样的

  return (
    <h1>
      {state.count} <br />
      {/* <button onClick={() => { dispatch({ type: 'STEP', payload: step }) }}>Step Up</button> */}
    </h1>
  )
}

export default Counter;

Solution 2: move reducer inside component

  • 因为dependency[], 因此全局只有一个setInterval
  • props的更新本身就会引起重新渲染,因此reducer里面读到的永远是最新的props
import React, { useEffect, useReducer } from 'react';

const initialState = { count: 0 };

const Counter = (props: any) => {
  const { step } = props;

  const reducer = (state, action) => {
    switch (action.type) {
      case 'TICK': return { ...state, count: state.count + step };
      default: throw new Error();
    }
  }
  const [state, dispatch] = useReducer(reducer, initialState);

  console.log('render');
  useEffect(() => {
    console.log('componentDidUpdate')
    const id = setInterval(() => {
      dispatch({ type: 'TICK' }); 
    }, 1000);
    return () => {
      clearInterval(id);
      console.log('clean');
    }
  }, []); 

  return (
    <h1>
      {state.count} <br />
    </h1>
  )
}

export default Counter;

case 3: put function outside of effect

  • 特征:
    • 函数本身的不会发生变化(stable)
    • 函数本身未引用任何外部变量,
  • useEffect中引用外部变量(仅仅是变量不包括函数)会被提醒要加入到dependency
  • 所以这里的fetchData被要求加入到deps中
  • 因为fetchData定义是在component内部,useEffect的外部,如果加入到deps中,一定会引起页面的无限刷新
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const SearchResult = () => {

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

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

  useEffect(() => {
    // 将fetchData放到这里其实效果一样,性能也基本上没有差异
    fetchData(); // 未引用任何变量,所以不会被提醒加入到deps中
  }, []) // 如果把函数加入到deps中,反而会造成页面的无限循环
  return (
    <div>{data.hits.map((item: any) => (
      <div>{item.title}</div>
    ))}</div>
  )
}

export default SearchResult;

case 4: function references any external variables

Solution : put function inside useEffect with variable as deps in useEffect

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const SearchResult = () => {
  const [query, setQuery] = useState('react');
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => { // define inside useEffect
      const result = await axios('https://hn.algolia.com/api/v1/search?query='+ query);
      setData(result.data);
    }
    fetchData();
  }, [query]) // add external variables here
  return (

    <div>
      <ul>
        {data.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>
      {query}
    </div>
  )
}

export default SearchResult;

case 5: function references any external variables but is used by multiple useEffects

Solution: useCallback

  • 特征:
    • 如果有一个函数必须放到useEffect之外,例如想要在多个useEffect中复用这个函数
    • 该函数要依赖外部变量时
  • 解决:
    • useCallback的使用能够使得函数在每次页面渲染时候保持稳定,因此不会引起页面的再次渲染
    • useCallback的本质其实是将原本定义在useEffect中的函数分离出来的一层,最终还是依赖于变量的
  • 问题:
    • 只是'缓存'了变量,并未缓存api请求的结果,可以进一步优化
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

const SearchResult = () => {
  const [query, setQuery] = useState('react');
  const [data1, setData1] = useState({ hits: [] });
  const [data2, setData2] = useState({ hits: [] });

  const MemorizedGetFetchUrl = useCallback(() => 'https://hn.algolia.com/api/v1/search?query=' + query, [query]);

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(MemorizedGetFetchUrl());
      setData1(result.data);
    }
    fetchData();
  }, [MemorizedGetFetchUrl])


  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(MemorizedGetFetchUrl())
      setData2(result.data);
    }
    fetchData()
  }, [MemorizedGetFetchUrl])

  return (
    <div>
      <ul>
        {data1.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>

      <br />
      <ul>
        {data2.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default SearchResult;

Optimization: useCallback

  • 这里优化,本质上是因为deps的缘故,只有在query改变时才会进行api请求
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

const SearchResult = () => {
  const [query1, setQuery1] = useState('react');
  const [query2, setQuery2] = useState('redux');
  const [refresh, setRefresh] = useState(0)
  const [data1, setData1] = useState({ hits: [] });
  const [data2, setData2] = useState({ hits: [] });

  const MemorizedGetFetchUrl = useCallback((query, id) => {
    const fetchData = async () => {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
      if (id === 1) setData1(result.data);
      if (id === 2) setData2(result.data);
    }
    fetchData()
  }, []);


  useEffect(() => {
    MemorizedGetFetchUrl(query1, 1);
  }, [MemorizedGetFetchUrl, query1])


  useEffect(() => {
    MemorizedGetFetchUrl(query2, 2);
  }, [MemorizedGetFetchUrl, query2])

  return (
    <div>
      <ul>
        {data1.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>

      <br />
      <ul>
        {data2.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>
      <button onClick={() => { setRefresh(refresh + 1) }}>Re-render Page</button>
    </div>
  )
}

export default SearchResult;

Case 6: clean up last api influence to avoid Race Condition

  • 何时需要cleanup函数?
    • useEffect存在depsdeps变化的时候
    • 例如在useEffect中的api请求依赖于某个变量
      • 因为无法保证api请求的返回顺序,我们需要在新的请求之前,禁止上一次请求的结果对页面元素产生影响
  • 代码分析:
    • 本质原因是js的闭包,闭包闭的variable而不是值
    • 因此在第一次useEffect执行未完成,但是unmount已经触发的情况下,第一次·的值会被修改为·
    • 这时候setData的操作就不会被出发
const SearchResult = () => {
  const [query, setQuery] = useState('');
  const [search, setSearch] = useState('')
  const [data, setData] = useState({ hits: [] });

  const MemorizedGetFetchUrl = useCallback((didCancel) => {
    const fetchData = async () => {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=' + search);
      !didCancel && setData(result.data); 
      !!didCancel && console.log('cancel setting result');
    }
    search !== '' && fetchData()
  }, [search]);


  useEffect(() => {
    let didCancel = false;
    !didCancel && MemorizedGetFetchUrl(didCancel);
    return () => {
      didCancel = true;
    }
  }, [MemorizedGetFetchUrl])

  return (
    <div>
      <ul>
        {data.hits.map((item: any) => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>
      <input type="text" value={query} onChange={evt => setQuery(evt.target.value)} />
      <button onClick={() => setSearch(query)}>Search</button>
    </div>
  )
}
export default SearchResult;

Case 7: how to choose between useMemo and useCallback

  • useCallback可以直接传参数,而useMemo直接参数, 但是二者都可以直接读取定义在文件内的变量
  • useMemo重点在与缓存了昂贵计算函数运行的结果,而useCallback的重点在于缓存了函数本身; 使用useCallback无法达到缓存昂贵计算的结果
  • useCallback中可以进行api请求,而useMemo内部传入函数时只适合做用在渲染过程中的昂贵计算上,比如重交互的图表动画
  • 结论: 单纯为了持久化函数或者useEffect函数复用一个函数,使用useCallback; 为了能够减少昂贵计算的次数,使用useMemo

Summary

  • useEffect依赖的变动会引起
    • unmount
    • render
    • componentDidUpdate
  • 什么会引起componentWillUnmount?
    • 页面退出
    • useEffect依赖值的变动
  • setState会引起什么?
    • render
    • componentDidUpdate
    • 不会引起unmount
  • props的改变会引起什么?
    • 子组件的render,如果是不必要的,可以使用memo + shallowEqual的方式避免子组件的重新渲染
    • 子组件的componentDidUpdate
    • 不会引起unmount
    • 不会引起子组件state的reset
import React, { useState, memo } from "react";
import shallowCompare from "react-addons-shallow-compare";

const App = () => {
  const [a, setA] = useState(0);
  console.log("app render");
  return (
    <div>
      <div>Hello World</div>
      {a}
      <button onClick={() => setA(a => a + 1)}>change app</button>
      <Counter a={a} />
    </div>
  );
};

const Counter = memo(({ a }: any) => {
  const [b, setB] = useState(a);
  console.log("counter render");
  return (
    <div>
      {b}
      <button onClick={() => setB(b => b + 1)}>change counter</button>
    </div>
  );
}, shallowCompare);

export default App;
  • 为什么要减少dependency?
    • 为了减少dependency变化带来的componentWillUnmount的执行
  • useEffect会提醒你把什么东西加入到deps中?
    • 外部变量
    • 包含外部变量函数
    • 不提醒你把不包含外部变量函数加入
  • 什么样的变量适合放入state中?
    • 用来沟通htmljs的变量
  • 定义在hooks之外,组件之内的变量和函数有什么特征?
    • 在每次渲染后,引用都会发生改变
  • 经验之谈:
    • 放在deps里面的不是props就是state, 共同特征: 会引起页面的重新渲染
    • 因为component中的hooks之外的变量和函数都是不稳定的,会引起无限渲染
    • 如果要放在component之外,说明不需要被复用,其实可以直接放入到hooks里面
    • 如果没办法放在component之内,有必须在hooks之外的(复用),则需要使用useCallback, useMemo来原始化
  • 本质: hooks的本质就是在immutable中使用mutable
  • useReducer vs useState: 前者具备了数据在上一次和此次沟通的能力,也便利了状态之间的沟通

References