React API

组件

React.Component 

React.Component 是使用 ES6 classes 方式定义 React 组件的基类:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

 

React.PureComponent 

React.PureComponent 与 React.Component 两者的区别在于 React.Component 并未实现 shouldComponentUpdate()

而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

注意:

React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。

仅在你的 props 和 state 较为简单时,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新时才使用 React.PureComponent

React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

 

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

React.memo 为高阶组件

React.memo 仅检查 props 变更。

如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

注意:

与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

 

转换元素

cloneElement():以 element 元素为样板克隆并返回新的 React 元素。

返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。

新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。

React.cloneElement(
  element,
  [props],
  [...children]
)

React.cloneElement() 几乎等同于:

<element.type {...element.props} {...props}>{children}</element.type>

isValidElement():验证对象是否为 React 元素.

React.Children

提供了用于处理 this.props.children 不透明数据结构的实用方法。

 

React.Children.map

React.Children.map(children, function[(thisArg)])

在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg

如果 children 是一个数组,遍历后还会为数组中的每个子节点调用该函数。

如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined

注意:

如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

 

React.Children.forEach

React.Children.forEach(children, function[(thisArg)])

 

React.Children.count

返回 children 中的组件总数量,等同于通过 map 或 forEach 调用回调函数的次数。

 

React.Children.only

验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

注意:

React.Children.only() 不接受 React.Children.map() 的返回值,因为它是一个数组而并不是 React 元素。

 

React.Children.toArray

将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。

当你想要在渲染函数中操作子节点的集合时,它会非常实用,

特别是当你想要在向下传递 this.props.children 之前对内容重新排序或获取子集时。

注意:

React.Children.toArray() 在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说,toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

 

Ref

createRef():创建一个能够通过 ref 属性附加到 React 元素的 ref

React.forwardRef()

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。

React.forwardRef 接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。

用途:

  • 转发refs到dom组件
  • 在高阶组建中转发refs
const FancyButton = React.forwardRef((props, ref) => (  
  <button ref={ref} className="FancyButton">    
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>
    Click me!
</FancyButton>;

在上述的示例中,React 会将 <FancyButton ref={ref}> 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 <button ref={ref}> 元素。

因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 <button> DOM 元素实例。

 

Suspense

Suspense 使得组件可以“等待”某些操作结束后,再进行渲染。

目前,Suspense 仅支持的使用场景是:通过 React.lazy 动态加载组件。它将在未来支持其它使用场景,如数据获取等。

React.lazy

// 这个组件是动态加载的
const SomeComponent = React.lazy(() => import('./SomeComponent'));

注意

使用 React.lazy 的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 polyfill 来使用该特性。

React.Suspense

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

注意:

React.lazy() 和 <React.Suspense> 尚未在 ReactDOMServer 中支持。这是已知问题,将会在未来解决。

 

React.component

组件的生命周期

Mounting阶段:

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

Updating阶段:

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

Unmounting阶段:

错误处理:

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

constructor()

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

如果不在其他语句之前调用 super(props),那么this.props 在构造函数中可能会出现未定义的 bug。

在 React 中,构造函数仅用于以下两种情况:

 

static getDerivedStateFormProps(props,state)

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。

它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

 

Render()

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 render()

 

componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。

依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前,但会导致性能问题。

 

shouldComponentUpdate(nextProps,nextState)

默认行为是 state 每次发生变化组件都会重新渲染。

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。

首次渲染或使用 forceUpdate() 时不会调用该方法。

如果你一定要手动编写此函数,可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render() 和 componentDidUpdate()

且当返回 false 时,仍可能导致组件重新渲染。

 

getSnapShotBeforeUpdate(preProps,preState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。

能在组件更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。应返回 snapshot 的值(或 null

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们是否在 list 中添加新的 items ?
    // 捕获滚动​​位置以便我们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之间可能存在延迟。

 

componentDidUpdate(preProps,preState,snapshot)

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。

可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

为什么props传给state会产生bug?

一个组件会接收多个 prop,任何一个 prop 的改变都会导致重新渲染和不正确的状态重置,数据就可能会丢失。

componentWillUnmount()

会在组件卸载及销毁之前直接调用。

可以在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

错误处理

static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { 
     hasError: false 
   };
 }

  static getDerivedStateFromError(error) {    
    // 更新 state 使下一次渲染可以显降级 UI    
    return { hasError: true };  
   }
  render() {
    if (this.state.hasError) {      
    // 你可以渲染任何自定义的降级  UI      
    return <h1>Something went wrong.</h1>;    
    }
    return this.props.children;
  }
}

componentDidCatch(error,info)

componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

其他API

setState()

将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。

React会批量延迟调用setState(),然后通过一次传递更新多个组件,所以无法在调用setState()后直接取state值。

componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

forceUpdate()

可以调用 forceUpdate() 强制让组件重新渲染。

调用 forceUpdate() 会使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()

但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。不推荐

class属性

defaultProps

defaultProps可以为class组件添加默认props,一般用于props未赋值但又不能为null的情况。

class CustomButton extends React.Component {
  // ...
}

CustomButton.defaultProps = {
  color: 'blue'
};

 

Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

为什么用Hook?

1.在组件之间复用状态逻辑很难。可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。

2.复杂组件变得难以理解。产生一些bug。Hook 将组件中相互关联的部分拆分成更小的函数,比如设置订阅或请求数据

3.难理解的class。比如class 不能很好的压缩、会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。Hook 使你在非 class 的情况下可以使用更多的 React 特性。

 

state Hook

在这里useState就是一个Hook,

useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。

它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。

import React, { useState } from 'react';
function Example() {
  // 声明一个叫 “count” 的 state 变量。  
 const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Hook是什么?

Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook

Hook 不能在 class 组件中使用,

Hook 使用了 JavaScript 的闭包机制。

什么时候用Hook?

 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。

 

useState和this.state提供的功能完全相同。

useState()唯一的参数是初始state

(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

 

 const [count, setCount] = useState(0);

这是数组解构。它意味着我们同时创建了 count和 setCount 两个变量,count 的值为 useState 返回的第一个值,setCount 是返回的第二个值。

它等价于下面的代码:

 var countStateVariable = useState(0); // 返回一个有两个元素的数组
  var count = countStateVariable[0]; // 数组里的第一个值
  var setCount = countStateVariable[1]; // 数组里的第二个值

 

class和Hook中state(useState和this.state)的区别:

1.this.state的赋值一定是对象,useState的赋值可以是数字、字符串、对象、数组

2.前者读取state是这样的写法:{this.state.xxx};后者可以直接{xxx}

3.前者需要通过this.setState()更新,后者setXXX(XXX+1)

  <button onClick={() => this.setState({ count: this.state.count + 1 })}>    
      Click me
  </button>
 <button onClick={() => setCount(count + 1)}>    
     Click me
  </button>

 

使用多个state变量

function ExampleWithManyStates() {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

在以上组件中,我们有局部变量 agefruit 和 todos,并且我们可以单独更新它们:

  function handleOrangeClick() {
    // 和 this.setState({ fruit: 'orange' }) 类似
    setFruit('orange');
  }

 

Effect Hook

在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”.

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:  
useEffect(() => {    // 使用浏览器的 API 更新页面标题    
  document.title = `You clicked ${count} times`;  
});
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Effect Hook分为需要清除的和不需要清除的:

无需清除的Effect

即在 React 更新 DOM 之后运行一些额外的代码。

比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。

因为我们在执行完这些操作之后,就可以忽略他们了。

class和Hook“副作用”区别?

1.class中副作用操作放到 componentDidMount 和 componentDidUpdate 函数中。

2.如果想让组件在加载和更新时执行同样的操作,class中只能在2个生命周期函数中写重复的代码。

   Hook中只用写一次,useEffect()默认情况下载第一次渲染后和每次更新后都会执行

3.使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕。class会。

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

componentDidMount() {    
 document.title = `You clicked ${this.state.count} times`;  
  }  
componentDidUpdate() {    
 document.title = `You clicked ${this.state.count} times`;  
}
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
import React, { useState, useEffect } from 'react';
function Example() {
  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

class:

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {    
     ChatAPI.subscribeToFriendStatus(      
       this.props.friend.id,      
       this.handleStatusChange    
     );  
  }  
  componentWillUnmount() {    
     ChatAPI.unsubscribeFromFriendStatus(      
       this.props.friend.id,      
       this.handleStatusChange    
     );  
  }  
  handleStatusChange(status) {    
     this.setState({      
         isOnline: status.isOnline    
     });  
  }
  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

Hook:

如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {    
     function handleStatusChange(status) {      
        setIsOnline(status.isOnline);    
     }    
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);     
       // Specify how to clean up after this effect:    
      return function cleanup() {      
           ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
      };  
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

React什么时候执行清除effect?

在组件卸载的时候执行清除操作。

 

Hook的使用规则

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最顶层调用 Hook。不要在循环、条件判断或者子函数中调用。【目的:确保 Hook 在每一次渲染中都按照同样的顺序被调用。】
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

 

Hook可以定义多个state变量,React怎么知道哪个state对于哪个state?

 React 靠的是 Hook 调用的顺序。

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

如果在条件、循环、嵌套中调用Hook,第一次可能没事,第二次渲染会跳过该 Hook,Hook 的调用顺序发生了改变。

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}
// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect

// ...

 

自定义Hook

自定义 Hook 可以让你在不增加组件的情况下在组件之间重用一些状态逻辑。

Hook 是一种复用状态逻辑的方式,它不复用 state 本身。

Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。

如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。原因是:可以帮助 linter 插件在使用 Hook 的代码中找到 bug。

 

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。

 

Hook API

基础Hook:

useState

函数式更新:

如果新的state需要通过之前的state算出来,可以通过将函数传给setState。setState接收之前的State然后返回更新后的值。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

注意

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。

惰性初始 state

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useEffect

赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 (条件effect)才执行。

清除effect

为防止内存泄漏,清除函数会在组件卸载前执行。

如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除

effect的执行时机

useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

但并非所有 effect 都可以被延迟执行。

例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。

effect的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。

在某些场景下这么做可能会矫枉过正。所以有了effect的第二个参数

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

只有当 props.source 改变后才会重新创建。

注意

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数,但effect 内部的 props 和 state 就会一直持有其初始值。

推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useContext

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

useContext 的参数必须是 context 对象本身

调用了 useContext 的组件总会在 context 值变化时重新渲染,如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

提示:

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);  
  return (    
    <button style={{ background: theme.background, color: theme.foreground }}>      
    I am styled by theme context!    
    </button>  
  );
}

额外Hook:

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

指定初始state

将初始 state 作为第二个参数传入 useReducer :

const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}  
);
function init(initialCount) {  return {count: initialCount};}
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':      return init(action.payload);    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);  
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>        
       Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

 

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值.

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

注意:无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。

useDebugValue(value)

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

 

Hooks FAQ

哪个版本的React包含Hook?

从16.8.0开始,React 在以下模块中包含了 React Hook 的稳定实现:

  • React DOM
  • React Native
  • React DOM Server
  • React Test Renderer
  • React Shallow Renderer

要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。如果你忘记更新诸如 React DOM 之类的 package,Hook 将无法运行。

有什么是 Hook 能做而 class 做不到的?

自定义Hook,且让函数组件有了state

Hook 能否覆盖 class 的所有使用场景?

目前暂时还没有对应不常用的 getSnapshotBeforeUpdategetDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。

目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook。

你可以继续使用之前使用的 API;它们仍会继续有效。

React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch 和 useSelector 等 hook。

React Router 从 v5.1 开始支持 hook

其它第三库也将即将支持 hook。

Hook 能和静态类型一起用吗?

Hook 在设计阶段就考虑了静态类型的问题。因为它们是函数,所以它们比像高阶组件这样的模式更易于设定正确的类型。最新版的 Flow 和 TypeScript React 定义已经包含了对 React Hook 的支持。

从 Class 迁移到 Hook,生命周期方法要如何对应到 Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState
  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  • shouldComponentUpdate:详见 下方 React.memo.
  • render:这是函数组件体本身。
  • componentDidMountcomponentDidUpdatecomponentWillUnmountuseEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  • getSnapshotBeforeUpdatecomponentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

如何获取上一轮的 props 或 state?

目前,通过ref来实现。

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);  
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {  
 const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

 

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。

 

什么情况下能把函数从依赖列表中省略?

只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); 
    // 使用了 productId prop    
    const json = await response.json();
    setProduct(json);
  }

  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`  // ...
}

推荐的修复方案是把那个函数移动到你的 effect 内部

如何避免向下传递回调?

我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch 函数:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提示:`dispatch` 不会在重新渲染之间变化  const [todos, dispatch] = useReducer(todosReducer);
  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

React 是如何把对 Hook 的调用和组件联系起来的?

每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值