了解函数化组件 HooksuseState,useEffect,useContext,useReducer

Posted 面条请不要欺负汉堡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了了解函数化组件 HooksuseState,useEffect,useContext,useReducer相关的知识,希望对你有一定的参考价值。

一. Hooks?


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

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。


二.为什么要使用 hooks?


1. 难以理解的 class 、组件中必须去理解 javascript 与 this 的工作方式、需要绑定事件处理器、纯函数组件与 class 组件的差异存在分歧、甚至需要区分两种组件的使用场景;Hook 可以使你在非 class 的情况下可以使用更多的 React 特性

2. 在组件之间复用状态逻辑很难、大型组件很难拆分和重构,也很难测试。Hook 使你在无需修改组件结构的情况下复用状态逻辑

3. 复杂组件变得难以理解、业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
注意:hook和class组件是不能够同时使用的,否则就会出现报错


三.hook 使用

Hook 就是JavaScript 函数,但是使用它们会有两个额外的规则:
1、只能在函数最外层调用 Hook。不要在循环、条件判断或者嵌套函数(子函数)中调用。
2、只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

(一)基础Hook

(1)state hook-useState 状态钩子


对于使用过class组件,相信对于state肯定有很深的印象,对于一些需要用到的全局变量,**在class组件中我们常常采用的方式是this.state = ,但是在hook中我们采用的方式就是使用useState这个hook,然后就可以对这种全局变量进行引用,在引用时只需要用其变量名即可**,

useState 状态钩子


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

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


等价的 class 示例

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

  render() 
    return (
      <div>
        <p>You clicked this.state.count times</p>
        <button onClick=() => this.setState( count: this.state.count + 1 )>Click me</button>
      </div>
    );
  


在 useState()中,它接受状态的初始值作为参数,即上例中计数的初始值,它返回一个数组,其中数组第一项为一个变量,指向状态的当前值。类似 this.state,第二项是一个函数,用来更新状态,类似 setState
上述例子中没有 class 继承、没有 this、没有生命周期、代码更加简洁、这就是使用 hooks 的意义;

总结:
 

1. 引入useState hooK: import React,  useState  from 'react';
2. 声明一个叫 "count" 的 state 变量:
  const [count, setCount] = useState(0);//useState(0),0是count的初始化值
3. 读取 State:  <p>You clicked count times</p>
4. 更新 State:
<button onClick=() => setCount(count + 1)>
    Click me
  </button>

声明多个 state 变量
可以在一个组件中多次使用 State Hook:

// 声明多个 state 变量
  const [age, setAge] = useState(42);//声明age ,初始化值为42
  const [fruit, setFruit] = useState('banana');//声明fruit,初始化值为banana
  const [todos, setTodos] = useState([ text: '学习 Hook' ]);声明todos,初始化值为text: '学习 Hook'


(2)Effect Hook -useEffect 副作用钩子

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。

1. 无需清除的 effect

在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作

 使用hook-useEffect副作用钩子,更新网页 title

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

等价使用 class :没有使用hook的情况下 副作用(数据更新)操作放到 componentDidMount 和 componentDidUpdate 函数中

class ClassTitle 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>
        <h1>2. 没有使用hook的情况下 副作用(数据更新)操作</h1>
        <p>You clicked this.state.count times</p>
        <button onClick=() => this.setState( count: this.state.count + 1 )>
          Click me
        </button>
      </div>
    )
  

总结:

useEffect可以用来更好的处理副作用,如异步请求等;可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合

useEffect(() => , [array]);

useEffect()接受两个参数,第一个参数是你要进行的异步操作,第二个参数是一个数组,用来给出 Effect 的依赖项。只要这个数组发生变化,useEffect()就会执行。当第二项省略不填时,useEffect()会在每次组件渲染时执行。这一点类似于类组件的 componentDidMount

实现一个 useEffect() 依赖项变化的例子

import React,  useState, useEffect  from 'react';

const AsyncCount = ( countNum ) => 
  const [loading, setLoading] = useState(true);
  const [count, setCount] = useState(0);

  useEffect(() => 
    setLoading(true);
    setTimeout(() => 
      setLoading(false);
      setCount(countNum);
    , 2000);
  , [countNum]);
  return <>loading ? <p>Loading...</p> : <p>count</p></>;
;

const TestCount = () => 
  const [count, setCount] = useState(0);
  const changeCount = (name) => 
    setCount(name);
  ;
  return (
    <>
      <AsyncCount countNum=count />
      <button
        onClick=() => 
          changeCount(count + 1);
        
      >
        增加
      </button>
      <button
        onClick=() => 
          changeCount(count - 1);
        
      >
        减少
      </button>
    </>
  );
;

export default TestCount;

再上述例子中,我们把处理 count 异步的操作以及是否渲染 loading,都放在了 AsyncCount hook 中;把复杂操作,通过 hooks 提取出去;将组件中关联部分拆分; 

那下面我们在做一个更加细化的拆分,拆出一个自己的 hook

const useCount = (countNum) => 
  const [loading, setLoading] = useState(true)
  const [count, setCount] = useState(0)

  useEffect(() => 
    setLoading(true)
    setTimeout(() => 
      setLoading(false)
      setCount(countNum)
    , 2000)
  , [countNum])
  return [loading, count]


const AsyncCount = ( countNum ) => 
  const [loading, count] = useCount(countNum)
  return <>loading ? <p>Loading...</p> : <p>count</p></>


const TestCount = () => 
  const [count, setCount] = useState(0)
  const changeCount = (count) => 
    setCount(count)
  
  return (
    <>
      <AsyncCount countNum=count />
      <button
        onClick=() => 
          changeCount(count + 1)
        
      >
        增加
      </button>
      <button
        onClick=() => 
          changeCount(count - 1)
        
      >
        减少
      </button>
    </>
  )

上述 AsyncCount 组件中再次将它的副作用操作拆分;在此组件中只关注渲染结果,useCount 接受一个数字,返回一个数组,数组中包括状态,与 count 两个结果;在我们使用 useCount 时,会根据我们传入参数的不同而返回不同的状态; 

2. 需要清除的Effect

就是需要在componentWillUnmount中清除掉,例如我们使用setInterval来更新当前时间。

class FriendStatus extends React.Component
    constructor(props)
        super(props);
        this.state =  nowTime: null;
        this.timer = null;
    
    componentDidMount()
        this.timer = setInterval(() => 
            this.setState(
                nowTime: new Date()
            )
        , 1000)
     
    componentWillUnmount()
        if (this.timer !== null) 
            clearInterval(timer);
        
    
    render()
        let time = this.state.nowTime;
        return(
           <div>time.toString()</div>
        )
    

使用hook,如下:

import React,  useState, useEffect  from 'react';
 
function FriendStatus(props) 
  const [nowTime, setNowTime] = useState(new Date());
 
  useEffect(() =>     
    let timer = setInterval(() => 
        setNowTime(new Date())
    , 1000)
    return () =>    // 返回一个清理函数
      clearInterval(timer);
    ;
  , []);
  return(<div>nowTime.toString()</div>)

effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。 

3. 通过跳过Effect来进行性能优化

每次渲染后都执行清理或者执行effect会导致性能问题。在class中我们在componentDidUpdate中添加prevProps和prevState的比较逻辑来解决;

componentDidUpdate(prevProps, prevState)
    if(this.state.count !== prevState.count)
        document.title = `点击了this.state.count次`
   

在hook中使用effect

 useEffect(() =>
    console.log('执行了--useEffect')
    document.title = `点击了count次`;
, [count]); // 在初次渲染和count发生变化时更新

如果第二数组参数为[],则Effect会在初次渲染执行一次及包含清除函数Effect再执行一次(可将上述代码中的[count]替换为[]测试);下面分别给出两种情况的执行代码 

// 带清除函数即为useEffect的第一个参数(函数)中再返回一个函数
// 不带清除函数+第二个参数为[];
// --> 整个生命周期只执行一次,相当于componentDidMount;
useEffect(() => 
    console.log('执行了--useEffect');
    document.title = `点击了$count次`;
, []);
 
// 带清除函数+第二个参数为[];
// --> 整个生命周期中执行了两次,相当于componentDidMount和componentWillUnmount;
useEffect(() => 
    console.log('执行了--useEffect');
    document.title = `点击了$count次`; 
    return () =>   // 相当于componentWillUnmount;
        console.log('执行了--清除函数');
        document.title = "";   
    
, [])

如果不加第二个数组参数,则Effect除了会在初次渲染执行一次外,还会在每次更新都执行。

(3)useContext() 共享状态钩子

如果需要在深层次组件之间共享状态,可以使用 useContext()。context 提供了一种在组件之间共享 props 的方式,而不必显示地通过组件树的逐层传递 props; useContext 钩子比原始 class 组件中使用 context 更为方便

接收一个由React.createContext()创建的context对象(此处定义为Mycontext),并返回这个context属性vaule绑定的值;通过useContext获取到最近的<Mycontext.Provider value="">的props传递的value值;如下用法所示

// Mycontext为React.createContext的返回值
const Mycontext = React.createContext();
<Mycontext.provider value=>
    ...
</Mycontext.provider>
--------------------------
const value = useContext(Mycontext);//调用了useContext的组件总会在context值变化时重新渲染。

 dome 如下:


import React,  useContext  from 'react';

//1.接收一个 context 对象并返回该 context 的当前值
const themes = 
    light: 
      foreground: '#000000',
      background: '#eeeeee',
    ,
    dark: 
      foreground: '#ffffff',
      background: '#222222',
    ,
  
// 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
const ThemeContext = React.createContext(themes.light)
function Toolbar(props) 
    return (
      <div>
        <ThemedButton />
      </div>
    )
  
  
  function ThemedButton() 
    //调用了useContext的组件总会在context值变化时重新渲染。
    const theme = useContext(ThemeContext)
    return (
      <button style= background: theme.background, color: theme.foreground >
        I am styled by theme context!
      </button>
    )
  
function ExampleHook() 
  return (
    <div>
      <ThemeContext.Provider value=themes.dark>
        <Toolbar />
      </ThemeContext.Provider>
    </div>
  )

export default ExampleHook

(二)额外的 Hook

(1)useReducer

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。简单的说就是常常用于管理一些复杂的状态,适合 action 比较多的场景

语法:

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

参数:

参数一:useReducer 接受一个reducer 函数,reducer 接受两个参数一个是 state 另一个是 action 。
参数二:useReducer接受一个初始state, initialArg。将初始 state 作为第二个参数传入 useReducer 是最简单的方法。
参数三:useReducer接受一个init函数,通过init(initialArg)来初始化 state 。这样可以惰性地创建初始 state。

返回值:

返回一个状态state和 dispath方法函数,state 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的。

由此可知,有两种不同初始化useReducer 的state的方式:一种是直接在第二个参数传入初始state;另一种是在第三个参数通过init()创建初始state。 

A 当两个参数时

import React,  useReducer  from 'react';
const init = 
 count: 0
;
 
function reducer(state, action)
  switch(action.type)
    case 'add': 
     return count: state.count + 1;
    case 'minus':
      return count: state.count - 1;
    default: throw new Error();
  

 
function TestReducer()
  const [state, dispatch] = useReducer(reducer, init);
  return (
    <div>
      count: state.count
      <ul>
        <li><button onClick=() => dispatch(type: 'add')>+</button></li>
        <li><button onClick=() => dispatch(type: 'minus')>-</button></li>
      </ul>
    </div>
  )

 
export default TestReducer;

三个参数

import React ,useReducerfrom 'react';

const App2 = () => 
    const initialState =  name:'张三' , location : '北京' , count : 0 
    const init = (v) => 
        console.log('v2',Object.prototype.toString.call(v)==='[object Object]') //判断是否是对象
        console.log('v',v)
        return v
    
    const reducer = ( state , action ) => 

        switch(action.type)
            case 'add':
                return 
                    ...state,
                    count : state.count + 1
                
            case 'minus':
                return 
                    ...state,
                    count:state.count - 1
                
                
            case 'reset':
                return init(action.payLoad)
            default : 
                throw Error
        
        
        
    
    const [state, dispatch] = useReducer(reducer, initialState , init)
    return (
        console.log('state',state),
        <div>
            <div>
                <button onClick=()=>dispatch(type:'add')>加号</button>
            </div>
                现在的值:state.count
            <div>
                <button onClick=()=>dispatch(type:'minus')>减号</button>
            </div>
            <div>
                <button onClick=()=>dispatch(type:'reset', payLoad : initialState)>重置</button>
            </div>
        </div>
    );
;

export default App2;

(2)useReducer + useContext 的组合

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

大部分人并不喜欢在组件树的每一层手动传递回调。尽管这种写法更明确,但这给人感觉像错综复杂的管道工程一样麻烦。

在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch 函数

import React,  useState, useEffect, useContext, useReducer  from 'react'
// 1. 声明一个变量
const init = 
  count: 1,

// 2. 创建需要共享的context
const ThemeContext = React.createContext(null)

function reducer(state, action) 
  switch (action.type) 
    case 'add':
      return  count: state.count + 1 
    case 'minus':
      return  count: state.count - 1 
    default:
      throw new Error()
  


// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) 
  return (
    <div
      style=
        backgroundColor: '#faad14',
        padding: '10px',
      
    >
      <p>这是子组件</p>
      <ThemedButton />
    </div>
  )


function ThemedButton(props) 
  // 5.使用共享 Context:如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。
  const  state, dispatch  = useContext(ThemeContext)
  const handleClick = () => 
    dispatch( type: 'add' )
  
  return (
    <div
      style=
        backgroundColor: '#f00',
        padding: '10px',
      
    >
      <p>这是孙组件:state.count</p>
      <button onClick=handleClick>点击</button>
    </div>
  )


export default function HookreduceContext() 
  //4. 创建useReducer 对象
  const [state, dispatch] = useReducer(reducer, init)
  // 3.使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
  return (
    <div
      style=
        backgroundColor: '#13ce66',
        padding: '10px',
        width: '200px',
        margin: 'auto',
        marginTop: '20px',
      
    >
      <ThemeContext.Provider value= state, dispatch >
        <p>这是父组件</p>
        <Toolbar />
      </ThemeContext.Provider>
    </div>
  )

useReducer + useContext 的组合,能够在子组件里面能够获取到上级组件传递过来的状态,并且能够进行修改

 总结:

1.useContext 创建全局状态,不用一层一层的传递状态。
2. useReducer 创建 reducer,并根据不同的 dispatch 更新 state。
3.代码写到哪里状态就加到哪里,不用打断思路跳到 redux 里面去写。
4. 全局状态分离,避免项目变大导致 Redux 状态树难以管理。

以上是关于了解函数化组件 HooksuseState,useEffect,useContext,useReducer的主要内容,如果未能解决你的问题,请参考以下文章

了解样式化组件中的 CSS 辅助函数

yii2源码分析之组件实例化流程

浅析vue封装自定义插件

在反应中将 ref 传递给样式化的组件

React Hooks-useMemo篇

react函数组件——use接收路由参数之HOOK函数获取——useSelector,useDispatch用法——antpc端表单的简单使用