Hook的常用方法

useState

useState接收一个初始值,返回的是一个解构出来的数组,第一个是当前状态(似state),第二个是状态的更新函数(似setState),更新函数与react的setState不同的是,useState的更新函数会将状态替换(replace)而不是合并(merge)。

使用场景:函数组件中需要用到状态值,类似react类组件的state

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量,0是默认值
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
export default Example;

默认值:useState()括号后面,可传入值,也可以传函数,该函数只会渲染一遍

const [ count, setCount ] = useState(() => {
  return props.count || 0
})

更新渲染:当我们在使用 useState 更新值时,组件重新渲染。若传入的更新值时为不变时,组件不会重新渲染。

useReducer

使用场景:由于useState的更新函数采用的是替换的方式,当我们要在函数组件中处理复杂状态时,如对象和数组等等,使用useState就不尽人意了。因此,我们使用useReducer来解决这一问题

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

它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法,initialArg为初始值。

import React, { useReducer } from 'react';
// 修改一个状态对象中的某属性,如果用useState,则要重新赋值一个对象,每个属性名都要写一遍
const initialState = {count: 0, name: '名字', age: 1};

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

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

init是个函数,并以initialArg为入参,用于操作initialArg,返回自定义的初始值。

import React, { useReducer } from 'react';
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 Example({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>
    </>
  );
}
export default Example;

useEffect

使用场景:useEffect副作用,使函数组件拥有了类似react的声明周期。useEffect会在组件每次render之后调用,useEffect有两个参数,第一个为执行函数,第二个为数组[]

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合

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

function Example() {
  const [count, setCount] = useState(0);
  const [dataSources, setDataSources] = useState([]);

  /* 
   * 情况一:useEffect无第二个参数 
   */
  //组件初始化和render后,都会执行该useEffect
  useEffect(() => {
    console.log("相当于生命周期:componentDidMount+componentDidUpdate")
  });

  /* 
   * 情况二:useEffect有第二个参数 
   */
  //第二个参数为空数组时:组件初始化才执行
  useEffect(() => { 
    console.log("相当于生命周期:componentDidMount"); 
  }, []);
  
  //第二个参数为指定状态值时:组件初始化时和dataSources发生变化才执行
  useEffect(() => { 
    console.log("相当于生命周期:componentDidMount")
    console.log("相当于依赖dataSources状态值的生命周期:componentDidUpdate")
  }, [dataSources]);

  //执行函数内return一个函数:初始化时执行函数体,组件卸载unmount时执行return后的函数
  useEffect(() => {
    console.log("相当于生命周期:componentDidMount")
    // 执行函数中直接使用return返回一个函数,这个函数会在组件unmount时执行。
    return () => {
      console.log('相当于声明周期:componentWillUnmount');  
    }
  }, []);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
export default Example;

官方提示:与 componentDidMount 或 componentDidUpdate 不同,useEffect是异步的,使用
useEffect 调度的 effect
不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。官方建议尽可能使用useEffect,effect 不需要同步地执行

在个别情况下(例如测量布局,页面状态值闪烁bug时),才使用useLayoutEffect代替useEffect, 形成同步,在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制

React.memo和useCallback和useMemo
React.memo
使用场景:React.memo就类似React.PureComponent,专门用于纯函数组件,起作用是对内部对象进行浅比较,判断是否重新渲染。
import React, { useState, useCallback } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>addCount</button>
      <button onClick={() => setList([...list, 1])}>changeList</button>
      <Child list={list} />
    </div>
  );
}
const Child = (props) => {
  console.log("进入了组件child")
  return (
    <div>这里是child:list为{props.list.join(',')}</div>
  )
}
export default Example;

此时,无论是改变count状态值,还是name状态值,Child组件都会重新渲染,如果我们想让Child组件在其props传入的值,即list改变时才渲染
用React.memo解决,修改Child如下

const Child = React.memo((props) => {
  console.log("进入了组件child")
  return (
    <div>这里是child:{props.list.join(',')}</div>
  )
})

useCallback

使用场景:useCallback用来缓存方法,类似react组件构造函数里面定义的:this.onChange =
this.onChange.bind(this),返回一个缓存的函数

上面例子还存在一个问题,如果Child组件传入一个函数作为参数,由于函数为引用类型,使得每次传入Child组件都是一个新的函数实例,如下

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

  const handleChange = () => {
    console.log(`selected`);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>addCount</button>
      <button onClick={() => setList([...list, 1])}>changeList</button>
      <Child list={list} handleChange={handleChange} />
    </div>
  );
}
const Child = React.memo((props) => {
  console.log("进入了组件child")
  return (
    <div>这里是child:list为{props.list.join(',')}</div>
  )
})
export default Example;

此时,无论是改变count状态值,还是list状态值,Child组件都会重新渲染
用useCallback解决,修改handleChange方法如下:

// 第二个参数为空数组,也可以指定依赖于某一状态值,即该状态值变化时才重新改变handleChange方法

const handleChange = useCallback(() => {
  console.log(`selected`);
},[])// 或[list]

useMemo

使用场景:useMemo函数用于缓存需计算操作的状态值,类似Vue的计算属性。第一个参数为计算函数,且必须return返回一个结果,返回一个缓存的值,第二个参数是一个数组[],useMemo执行依赖于次数组的状态值

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

  const [a, setA] = useState(0)
  const [b, setB] = useState(0)

  const handleChange = useCallback(() => {
    console.log(`selected`);
  },[])

  //使用useMemo缓存一个计算值,计算函数的执行依赖于状态值a和b,当a和b变化时才执行计算函数
  const memoizedValue = useMemo(() => a + b, [a, b]);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>addCount</button>
      <button onClick={() => setList([...list, 1])}>changeList</button>
      <button onClick={() => { setA(a+1)}}>点我A</button>
      <button onClick={() => { setB(b+1)}}>点我B</button>
      <Child list={list} memoizedValue={memoizedValue} handleChange={handleChange} />
    </div>
  );
}
const Child = React.memo((props) => {
  console.log("进入了组件child")
  return (
    <div>这里是child:list为{props.list.join(',')}, 计算总和:{props.memoizedValue}</div>
  )
})
export default Example;

useRef和React.forwardRef

useRef

useRef可以接受一个默认值,并返回一个含有current属性的可变对象;

使用场景:
1、获取子组件的实例(子组件需为react类继承组件);

2、获取组件中某个DOM元素;

3、用做组件的全局变量,useRef返回对象中含有一个current属性,该属性可以在整个组件色生命周期内不变,不会因为重复render而重复申明,类似于react类继承组件的属性this.xxx一样。

原因:由于useState保存的变量会触发组件render,而使用useRef定义全局变量不会触发组件render

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

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

  // 获取DOM元素span
  const spanEl = useRef(null);
  const getSpanRef = () => {
    console.log(2, spanEl.current)
  };
  // 获取react类组件实例
  const sunEl = useRef(null);
  const getRefBySun = () => {
    console.log(1, sunEl.current)
  };
  // 全局变量isClick,默认值false赋予isClick.current,相当于react类组件的this.isClick = false
  const isClick = useRef(false);
  const addCount = () => {
    if (!isClick.current) {
      setCount(count + 1)
      isClick.current = true
    }
  };
  return (
    <>
      <button onClick={addCount}>addCount</button>
      <Sun ref={sunEl} count={count} />
      <span ref={spanEl}>我是span</span>
      <button onClick={getSpanRef}>获取DOM元素Span</button>
      <button onClick={getRefBySun}>获取Sun组件</button>
    </>
  );
}
//Sun子组件
class Sun extends React.Component {
  render () {
    const { count } = this.props
    return (
      <div>{ count }</div>
    )
  }
}
export default Example;

React.forwardRef:

使用场景:比较少用,用于在父组件获取子组件的DOM元素作为自己的ref,然后操作子组件的DOM元素

import React, { useRef } from 'react';

function Example() {

  //ref
  const inputEl = useRef(null);
  const onButtonClick = () => {
    console.log(inputEl)
    inputEl.current.focus();
  };
  return (
    <>
      <TextInputWithFocusButton ref={inputEl} onButtonClick={onButtonClick} />
      <span>我是span</span>
    </>
  );
}
// 子组件
const TextInputWithFocusButton = (props) => {
  return (
    <>
      <input ref={props.ref} type="text" />
      <button onClick={props.onButtonClick}>Focus the input</button>
    </>
  );
}
export default Example;

上面代码报错:Warning: TextInputWithFocusButton: ref is not a prop.

需使用React.forwardRef包裹注入一个新的ref,修改TextInputWithFocusButton组件如下:

const TextInputWithFocusButton = React.forwardRef((props, ref) => {
  return (
    <>
      <input ref={ref} type="text" />
      <button onClick={props.onButtonClick}>Focus the input</button>
    </>
  );
})

useContext

使用场景:useContext需要和React.createContext结合起来使用,解决夸组件间的数据传递问题,类似redux。

使用useContext方式

import React, { useRef, useContext } from 'react';
//Context,createContext接收一个参数为默认值
const ThemeContext = React.createContext('white');
const AgeContext = React.createContext();

function Example() {
  return (
    <>
      <ThemeContext.Provider value={'blue'}>
        <div>
          <ChildOfContext />
        </div>
      </ThemeContext.Provider>
      <span>我是span</span>
    </>
  );
}
// 子组件
const ChildOfContext = (props) => {
  console.log("进入了子组件ChildOfContext")
  return (
    <div>
      这里是子组件ChildOfContext
       <GrandChildOfContext />
    </div>
  )
}
// 孙子组件
const GrandChildOfContext = (props) => {
  console.log("进入了孙子组件GrandChildOfContext")
  const color = useContext(ThemeContext);
  return (
    <div>
      这里是子组件GrandChildOfContext
      颜色是:{color} 
    </div>
  )
}
export default Example;

使用传统React API方式,修改孙子组件如下:

const GrandChildOfContext = (props) => {
  console.log("进入了孙子组件GrandChildOfContext")
  // 使用 Consumer 从上下文中获取 value
  return (
    <ThemeContext.Consumer>
      {value => (
        <div>
        这里是子组件GrandChildOfContext
        颜色是:{color} 
      </div>
      )}
    </ThemeContext.Consumer>
    
  )
}

使用contextType方式,只支持React类组件,修改孙子组件如下:

class GrandChildOfContext extends React.Component {
  static contextType = ThemeContext
  render () {
    const color = this.context
    return (
      <div>
        这里是子组件GrandChildOfContext
         颜色是:{color}
      </div>
    )
  }
}

多个context嵌套使用时使用useContext方式

import React, { useRef, useContext } from 'react';
//Context,createContext接收一个参数为默认值
const ThemeContext = React.createContext('white');
const AgeContext = React.createContext();

function Example() {
  return (
    <>
      <ThemeContext.Provider value={'blue'}>
        <div>
          <ChildOfContext />
        </div>
      </ThemeContext.Provider>
      <span>我是span</span>
    </>
  );
}
// 子组件进行嵌套一层Context
const ChildOfContext = (props) => {
  console.log("进入了子组件ChildOfContext")
  return (
    <div>
      这里是子组件ChildOfContext
      <AgeContext.Provider value={21}>
        <div>
          <GrandChildOfContext />
        </div>
      </AgeContext.Provider>
    </div>
  )
}
// 孙子组件
const GrandChildOfContext = (props) => {
  console.log("进入了孙子组件GrandChildOfContext")
  const color = useContext(ThemeContext);
  const age = useContext(AgeContext);
  return (
    <div>
      这里是子组件GrandChildOfContext
      颜色是:{color} 
      嵌套Context
      年龄是:{age} 
    </div>
  )
}
export default Example;

使用传统React API方式,修改孙子组件如下:
// 传统reactAPI方式

const GrandChildOfContext = (props) => {
  console.log("进入了孙子组件GrandChildOfContext")
  return (
    <ThemeContext.Consumer>
      {color => (
        <div>
          这里是子组件GrandChildOfContext
          颜色是:{color} 
          <AgeContext.Consumer>
            {age => (
              <div>
              嵌套Context
              年龄是:{age} 
            </div>
            )}
          </AgeContext.Consumer>
      </div>
      )}
    </ThemeContext.Consumer>
  )
}

contextType方式不支持多个Context嵌套,由此可见,useContext方式最为简单

官方提醒:接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的

context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value 值。即使祖先使用 React.memo 或
shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
因此,<MyContext.Provider>包裹的组件越多,嵌套越多,对子组件的影响越大,导致不必要的重渲染太多,所以Context最好就近用,别跨那么多级组件

Hook 使用规则

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

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。 只能在 React 的函数组件中调用 Hook。不要在其他
JavaScript 函数中调用。

import React, { useState } from 'react';

let isName = true;
function Example() {
  //报错:React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render 
  // 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  const [count, setCount] = useState(0);
  if(isName){
    const [name, setName] = useState('名字');
    isName = false;
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容