useEffect完全指南笔记

1. 渲染过程

第一次:

  • React: 给我状态为0时候的UI
  • 你的组件
  1. 给你需要渲染的内容:
  

You clicked 0 times

  1. 记得在渲染之后调用这个effect:
() => { document.title = 'You clicked 0 times' }。
  • React: 没问题。开始更新UI,喂浏览器,我要给DOM添加一些东西。
  • Browser: 酷,我已经把它绘制到屏幕上啦。
  • React:好的,我要开始运行我的effect啦:
    1. 运行
() => { document.title = 'You clicked 0 times' }。

点击之后:

  • 你的组件: 喂 React,把我的状态设置为1。
  • React: 给我状态为1的UI。
  • 你的组件
  1. 给你需要渲染的内容:

You clicked 1 times

2.记得在渲染完成之后,调用这个effect:

() => { document.title = 'You clicked 1 times' }。
  • React: 没问题。开始更新UI,喂浏览器,我修改了DOM。
  • Browser:酷,我已经将更改绘制到屏幕上啦。
  • React:好的,我现在开始运行属于这次渲染的effect:
    1. 运行
() => { document.title = 'You clicked 1 times' }。

2. 逆潮而动

到目前为止,我们可以明确地喊出下面重要的事实: 每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的propsstate

在组件内什么时候去读取propsstate是无关紧要的。因为他们不会改变。在单次渲染的范围内,propsstate始终保持不变。(解构赋值props使得这一点更明显。)

当然,有的时候你可能想在effect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法就是refs
这篇文章的最后一部分介绍了相关内容。

需要注意的是当你想要从过去渲染中的函数里读取未来propsstate,你是在逆潮而动。虽然它并没有(有时候可能也需要这样做),但它因为打破了默认范式会使代码显得不够“干净”。这是Hooks有意而为之的,因为它能帮助突出哪些代码是脆弱的,是需要依赖时间次序的。在class中,如果发生这种情况就没那么显而易见了。

下面这个计数器版本模拟了class中的行为:

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

3. Effect中的清理

像文档中解释的,有些effects可能需要有一个清理步骤。本质上,它的目的是消除副作用(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只会在浏览器绘制后运行effects。这使得你的应用更流畅,因为大多数effects并不会阻塞屏幕的更新。effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:

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

effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值:

// First render, props are {id: 10}
function Example() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
      // Cleanup for effect from first render
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
      };
    }
  );
  // ...
}

// Next render, props are {id: 20}
function Example() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
      // Cleanup for effect from second render
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
      };
    }
  );
  // ...
}

王国会崛起转而复归尘土,太阳会脱落外层变为白矮星,最后的文明也迟早会结束。但是第一次渲染中effect的清除函数只能看到{id: 10}这个props。

这正是为什么React能做到在绘制后立即处理effects — 并且默认情况下使你的应用运行更流畅。如果你的代码需要依然可以访问到老的props。

4. 告诉React去对比你的Effects

React并不能猜测到函数做了什么如果不先调用的话。
如果想要避免effects不必要的重复调用,你可以提供给useEffect一个依赖数组参数(deps):

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

这好比你告诉React:“Hey,我知道你看不到这个函数里的东西,但我可以保证只使用了渲染中的name,别无其他。”
如果当前渲染中的这些依赖项和上一次运行这个effect的时候值一样,因为没有什么需要同步React会自动跳过这次effect:

const oldEffect = () => { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];

const newEffect = () => { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];

// React can't peek inside of functions, but it can compare deps.
// Since all deps are the same, it doesn’t need to run the new effect.

5. 两种诚实告知依赖的方法

有两种诚实告知依赖的策略。你应该从第一种开始,然后在需要的时候应用第二种。
第一种策略是在依赖中包含所有effect中用到的组件内的值。让我们在依赖中包含count:

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

现在依赖数组正确了。虽然它可能不是太理想但确实解决了上面的问题。现在,每次count修改都会重新运行effect,并且定时器中的setCount(count + 1)会正确引用某次渲染中的 count值:

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
        setCount(0 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [0] // [count]
  );
  // ...
}

// Second render, state is 1
function Counter() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      const id = setInterval(() => {
        setCount(1 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [1] // [count]
  );
  // ...
}

这能解决问题但是我们的定时器会在每一次count改变后清除和重新设定。这应该不是我们想要的结果:(依赖发生了变更,所以会重新运行effect。)

第二种策略是修改effect内部的代码以确保它包含的值只会在需要的时候发生变更。
我们不想告知错误的依赖--我们只是修改effect使得依赖更少。

6. 移除依赖的常用技巧。

  • 让Effects自给自足

当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式

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

我喜欢把类似这种情况称为“错误的依赖”。是的,因为我们在effect中写了setCount(count + 1)所以count是一个必需的依赖。但是,我们真正想要的是把count转换为count+1,然后返回给React。可是React其实已经知道当前的count我们需要告知React的仅仅是去递增状态 - 不管它现在具体是什么值。

这正是setCount(c => c + 1)做的事情。你可以认为它是在给React“发送指令”告知如何更新状态。这种“更新形式”在其他情况下也有帮助,比如你需要批量更新。

注意我们做到了移除依赖,并且没有撒谎。我们的effect不再读取渲染中的count值。

尽管effect只运行了一次,第一次渲染中的定时器回调函数可以完美地在每次触发的时候给React发送c => c + 1更新指令。它不再需要知道当前的count值。因为React已经知道了。

  • 函数式更新

只在effects中传递最小的信息会很有帮助。类似于setCount(c => c + 1)这样的更新形式比setCount(count + 1)传递了更少的信息,因为它不再被当前的count值“污染”。它只是表达了一种行为(“递增”)。“Thinking in React”也讨论了如何找到最小状态。原则是类似的,只不过现在关注的是如何更新。

表达意图(而不是结果)和Google Docs如何处理共同编辑异曲同工。虽然这个类比略微延伸了一点,函数式更新在React中扮演了类似的角色。它们确保能以批量地和可预测的方式来处理各种源头(事件处理函数,effect中的订阅,等等)的状态更新。

然而,即使是setCount(c => c + 1)也并不完美。它看起来有点怪,并且非常受限于它能做的事。举个例子,如果我们有两个互相依赖的状态,或者我们想基于一个prop来计算下一次的state,它并不能做到。幸运的是, setCount(c => c + 1)有一个更强大的姐妹模式,它的名字叫useReducer

  • 解耦来自Actions的更新

我们来修改上面的例子让它包含两个状态:countstep。我们的定时器会每次在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,我们就把它加到依赖里。所以这也是为什么代码能运行正确。

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。
当你写类似setSomething(something => ...)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

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

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

useEffect(() => {
  const id = setInterval(() => {
    dispatch({type: 'tick'}); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id)
}, [dispatch]);

 return (
    <>
      

{count}

{ dispatch({ type: 'step', step: Number(e.target.value) }); }} /> );

React会保证dispatch在组件的声明周期内保持不变,所以上面例子中不再需要重新订阅定时器。

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

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

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();
  }
}
  • 为什么useReducer是Hooks的作弊模式(依赖props去计算下一个状态)

我们已经学习到如何移除effect的依赖。不管状态更新是依赖上一个形态还是依赖另一个状态。但假如我们需要依赖props去计算下一个状态呢?
举个例子,也许我们的API是
实际上, 我们可以避免依赖props.step,我们可以把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 

{count}

; }

这种模式会使一些优化失效,所以你应该避免滥用它。不过如果你需要你完全可以在reducer里面访问props。

即使在这个例子中,React也保证dispatch在每次渲染中都是一样的。所以你可以在以来中去掉它。它不会引起effect不必要的重复执行。
你可能会疑惑:这怎么可能?在之前渲染中调用的reducer怎么“知道”新的props?答案是当你dispatch的时候,React只是记住了action - 它会在下一次渲染中再次调用reducer。在那个时候,新的props就可以被访问到,而且reducer调用也不是在effect里。

这就是为什么我倾向认为useReducer是Hooks的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的effect调用。

7. 把函数移到Effects里

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

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

useEffect的设计意图就是要强迫你关注数据流的改变,然后决定我们的effects该如何和它同步 - 而不是忽视它直到我们的用户遇到了bug。

8.不能把这个函数放到Effect里

有时候你可能不想把函数移入effect里。比如,组件内有几个effect使用了相同的函数,你不想在每个effect里复制黏贴一遍这个逻辑。也或许这个函数是一个prop。
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 ...
  }, []); //  Missing dep: getFetchUrl

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

  // ...
}

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

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

function SearchResults() {
  //  Re-triggers all effects on every render
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); //  Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); //  Deps are correct but they change too often

  // ...
}

一个可能的解决办法是把getFetchUrl从依赖中去掉。但是,我不认为这是好的解决方式。这会使我们后面对数据流的改变很难被发现从而忘记去处理。这会导致类似于上面“定时器不更新值”的问题。

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

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

// ✅ Not affected by the data flow
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 ...
  }, []); // ✅ Deps are OK

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

  // ...
}

你不再需要把它设为依赖,因为它们不在渲染范围内,因此不会被数据流影响。它不可能突然意外地依赖于props或state。

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

function SearchResults() {
  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

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

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

  // ...
}

useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

我们来看看为什么这种方式是有用的。之前,我们的例子中展示了两种搜索结果(查询条件分别为'react''redux')。但如果我们想添加一个输入框允许你输入任意的查询条件(query)。不同于传递query参数的方式,现在getFetchUrl会从状态中读取。

我们很快发现它遗漏了query依赖:

function SearchResults() {
  const [query, setQuery] = useState('react');
  const getFetchUrl = useCallback(() => { // No query argument
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []); //  Missing dep: query
  // ...
}

如果我把query添加到useCallback 的依赖中,任何调用了getFetchUrl的effect在query改变后都会重新运行:

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

  // ✅ Preserves identity until query changes
  const getFetchUrl = useCallback(() => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, [query]);  // ✅ Callback deps are OK

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

  // ...
}

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

这正是拥抱数据流和同步思维的结果。对于通过属性从父组件传入的函数这个方法也适用:

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

  // ✅ Preserves identity until query changes
  const fetchData = useCallback(() => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
    // ... Fetch data and return it ...
  }, [query]);  // ✅ Callback deps are OK

  return 
}

function Child({ fetchData }) {
  let [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); // ✅ Effect deps are OK

  // ...
}

因为fetchData只有在Parentquery状态变更时才会改变,所以我们的Child只会在需要的时候才去重新请求数据。

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

有趣的是,这种模式在class组件中行不通,并且这种行不通恰到好处地揭示了effect和生命周期范式之间的区别。考虑下面的转换:

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

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  render() {
    // ...
  }
}

你可能会想:“少来了,我们都知道useEffect 就像componentDidMountcomponentDidUpdate的结合,你不能老是破坏这一条!”好吧,就算加了componentDidUpdate照样无用:

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() {
    // ...
  }
}

当然如此,fetchData是一个class方法!(或者你也可以说是class属性 - 但这不能改变什么。)它不会因为状态的改变而不同,所以this.props.fetchDataprevProps.fetchData始终相等,因此不会重新请求。那我们删掉条件判断怎么样?

  componentDidUpdate(prevProps) {
    this.props.fetchData();
  }

等等,这样会在每次渲染后都去请求。(添加一个加载动画可能是一种有趣的发现这种情况的方式。)也许我们可以绑定一个特定的query?

render() {
    return ;
  }

但这样一来,this.props.fetchData !== prevProps.fetchData表达式永远是true,即使query并未改变。这会导致我们总是去请求。

想要解决这个class组件中的难题,唯一现实可行的办法是硬着头皮把query本身传入 Child 组件。Child 虽然实际并没有直接使用这个query的值,但能在它改变的时候触发一次重新请求:

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

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

在使用React的class组件时,习惯于把不必要的props传递下去并且破坏父组件的封装。

在class组件中,函数属性本身并不是数据流的一部分。组件的方法中包含了可变的this变量导致我们不能确定无疑地认为它是不变的。因此,即使我们只需要一个函数,我们也必须把一堆数据传递下去仅仅是为了做“diff”。我们无法知道传入的this.props.fetchData 是否依赖状态,并且不知道它依赖的状态是否改变了。

使用useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。感谢周到的useCallback,属性比如props.fetchData的改变也会自动传递下去。

类似的,useMemo可以让我们对复杂对象做类似的事情。

function ColorPicker() {
  // Doesn't break Child's shallow equality prop check
  // unless the color actually changes.
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return ;
}

我想强调的是,到处使用useCallback是件挺笨拙的事。当我们需要将函数传递下去并且函数会在子组件的effect中被调用的时候,useCallback 是很好的技巧且非常有用。或者你想试图减少对子组件的记忆负担,也不妨一试。但总的来说Hooks本身能更好地避免传递回调函数。

10.说说竞态

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

有问题的原因是请求结果返回的顺序不能保证一致。比如我先请求 {id: 10},然后更新到{id: 20},但{id: 20}的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值。

这被叫做竞态,这在混合了async / await(假设在等待结果返回)和自顶向下数据流的代码中非常典型(propsstate可能会在async函数调用过程中发生改变)。

Effects并没有神奇地解决这个问题,尽管它会警告你如果你直接传了一个async 函数给effect。(我们会改善这个警告来更好地解释你可能会遇到的这些问题。)

如果你使用的异步方式支持取消,那太棒了。你可以直接在清除函数中取消异步请求。

或者,最简单的权宜之计是用一个布尔值来跟踪它:

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;

    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [id]);

  // ...
}

这篇文章讨论了更多关于如何处理错误和加载状态,以及抽离逻辑到自定义的Hook。我推荐你认真阅读一下如果你想学习更多关于如何在Hooks里请求数据的内容。

十一.提高水准

在class组件生命周期的思维模型中,副作用的行为和渲染输出是不同的。UI渲染是被props和state驱动的,并且能确保步调一致,但副作用并不是这样。这是一类常见问题的来源。

而在useEffect的思维模型中,默认都是同步的。副作用变成了React数据流的一部分。对于每一个useEffect调用,一旦你处理正确,你的组件能够更好地处理边缘情况。

然而,用好useEffect的前期学习成本更高。这可能让人气恼。用同步的代码去处理边缘情况天然就比触发一次不用和渲染结果步调一致的副作用更难。

这难免让人担忧如果useEffect是你现在使用最多的工具。不过,目前大抵还处理低水平使用阶段。因为Hooks太新了所以大家都还在低水平地使用它,尤其是在一些教程示例中。但在实践中,社区很可能即将开始高水平地使用Hooks,因为好的API会有更好的动量和冲劲。

我看到不同的应用在创造他们自己的Hooks,比如封装了应用鉴权逻辑的useFetch或者使用theme context的useTheme 。你一旦有了包含这些的工具箱,你就不会那么频繁地直接使用useEffect。但每一个基于它的Hook都能从它的适应能力中得到益处。

目前为止,useEffect主要用于数据请求。但是数据请求准确说并不是一个同步问题。因为我们的依赖经常是[]所以这一点尤其明显。那我们究竟在同步什么?

长远来看, Suspense用于数据请求 会允许第三方库通过第一等的途径告诉React暂停渲染直到某些异步事物(任何东西:代码,数据,图片)已经准备就绪。

当Suspense逐渐地覆盖到更多的数据请求使用场景,我预料useEffect 会退居幕后作为一个强大的工具,用于同步props和state到某些副作用。不像数据请求,它可以很好地处理这些场景因为它就是为此而设计的。不过在那之前,自定义的Hooks比如这儿提到的是复用数据请求逻辑很好的方式。


阅读原文

你可能感兴趣的:(useEffect完全指南笔记)