文章目录

  • Hook
  • useState
  • useEffect
  • useRef
  • useReducer
  • useContext
  • 自定义Hook
  • Hook使用规则


Hook

Hooks 是react 16.8版本新增的一项特性,可以在不编写class的情况下使用state以及其他的react特性

useState

用于引入类组件的state特性

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

export default function Counter() {
    
    // 使用解构接收useState()的值
    const [count, setCount] = useState(0)    // 使用useState创建一个默认值,用解构接收结果,第一个接收默认值,第二个接收更改state的方法
    
    // 监听类似于生命周期中的componentDidMout() 在初次挂载和状态更新时会执行 需要传入一个回调函数
    useEffect(() => {
        document.title = `当前数量为${count}`   // 模板字符
    })

    return (
            <button onClick={() => {setCount(count - 1) }}>-</button>   
            <span>{count}</span>                            
            <button onClick={() => {setCount(count + 1) }}>+</button>
            
        </div>
    )
}

修改 state 的方法

const [count, setCount] = useState(0)

...
// 第一种用法,直接接收一个新的state结果
setCount(count + 1)

// 第二种用法,接收一个方法,该方法返回一个新的state结果
setCount(() => {
   // 其他操作
   return count + 1
})

修改一个组件的state值会令组件重新渲染,但需要注意的是,如果对方是一个引用类型,修改引用类型内部的值,实际上组件对state这个引用类型的引用地址是不会发生改变的,因此会判断为state没有发生改变,不会出现渲染组件

// 修改obj内部的count时,即使内部已经发生改变,但是state的obj地址没有发生改变,因此不会重新渲染
import React, { useState } from 'react';

const Demo = () => {
    const [obj, setCout] = useState({count:0})	
    const [num, setNum] = useState(0)	// num的值发生变化时会重新渲染

    const changeState = () => {
        return setCout(() => {
            obj.count += 1
            return obj
        })
    }
   
    return(
        <div>
            <div>当前count的值为:{obj.count}</div>
            <button onClick={changeState}>自增按钮</button>
            <button onClick={() => setNum(num + 1)}>更新渲染按钮</button>
        </div>
    )

}
export default Demo

假如一个组件有多个状态值

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}

useEffect

用于引入类组件的生命周期特性

可以直接在函数组件内处理生命周期事件,它是 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合(组件渲染,更新,销毁)

第一个参数为一个函数,它会在组件渲染时执行函数内的内容,如果该函数A返回了一个函数B,则会在组件销毁前执行B函数(如果函数B想声明再返回一个函数C,会报错)

// LifecycleDemo.tsx
import React, {useEffect} from 'react'

const LifecycleDemo = () => {
    useEffect(() => {
        console.log('渲染')

        return () => {
            console.log('销毁')
        }

    },)

    return <div>LifecycleDemo组件</div>
}


export default LifecycleDemo
// App.tsx
import React, {useState} from "react";
import LifecycleDemo from "./LifecycleDemo";

function App() {
    // 使用一个状态来控制组件的渲染和销毁
    const [isShow, setIsShow] = useState(true)
    const toggle = () => {
        setIsShow(!isShow)
    }

    return (
        <div className="App">
            <button onClick={toggle}>控制组件渲染/销毁开关</button>
            {isShow && <LifecycleDemo />}   {/* 如果isShow为true则渲染该组件的简易写法 */}
        </div>
    );
}

export default App;
上面代码的效果是:
	在组件渲染时,打印:'渲染'
	在组件销毁时,打印:'销毁'

此时会存在一个现象:当组件并非渲染和销毁阶段,而是更新阶段时,则会打印:销毁 → 渲染,这说明 useEffect 在每次渲染后运行(默认情况下),并且可以选择在再次运行之前自行清理

注:这里的更新指的是所有能触发 LifecycleDemo 组件重新渲染的情况,如上面的代码,如果App的某个state状态发生改变,会导致整个 App 组件重新渲染,因此也会导致 LifecycleDemo 重新渲染(有时候会忽略这点)

因此与其将 useEffect 看作一个函数来完成3个独立生命周期的工作,不如将它简单地看作是在渲染之后执行副作用的一种方式,包括在每次渲染之前和卸载之前咱们希望执行的需要清理的东西


阻止每次重新渲染都会执行useEffect

useEffect( ) 的第二个参数为可选参数,类型为一个数组,它的作用是比较上一次作用的状态值,如果两次状态值没有发生改变,则不会执行本次的函数,实现了性能优化

这个值一般指 props 和 state,因为这个两个发生变化会重新渲染导致触发 useEffect,当然存一个能跨周期储存的普通变量也可以

const [value, setValue] = useState(xxx);
 
useEffect(() => {
  	...
}, [value]) 	// 仅在value更改时执行

这个数组可以传递任意多项,任意一项的数组发生改变时,都会执行 useEffect

const [value, setValue] = useState(xxx);
 
useEffect(() => {
  	...
}, [value1, value2, value3 ...]) 	// 仅在value更改时执行

绑定事件时需要在更新组件时销毁事件

这点可以扩展,不只是时间,如计时器这种也是需要清除的

import React, { useState, useEffect } from 'react';
const Demo: React.FC = () => {
    const [positions, setPositions] = useState({x:0, y:0})

    useEffect(() => {
        const updataMouse = (e: MouseEvent) => {
            console.log('inner')	
            setPositions({x: e.clientX, y: e.clientY})
        }
        document.addEventListener('click', updataMouse)
        
        // 如果不清除事件,则每次更新state时都会生成新的事件,会指数级增长,大大影响性能
        return () => {
            document.removeEventListener('click', updataMouse)
        }
    })

    return(
        <p>X: {positions.x}, Y:{positions.y}</p>
    )
   
}
export default Demo

执行顺序

import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
    const [count, setCout] = useState(0);

    useEffect(() => {
        console.log('执行useEffect')
        ref.current = count
    })
    
    Promise.resolve().then(() => {
        console.log('执行Promise')
    })
    
    setTimeout(() => {
        console.log('执行setTimeout')
    })

    return(
        <div>
            <button onClick={() => setCout(count + 1) }>自增按钮</button>            
        </div>
    )

}
export default Demo
打印顺序:执行Promise → 执行useEffect → 执行setTimeout

可以看出 useEffect 是在组件挂载完毕后才执行的异步函数(即晚于return内组件的渲染),晚于 Promise 执行,早于 setTimeout 执行

由于是渲染后才执行,因此可以利用这个特性,在渲染更新后还能拿到上一次渲染前的数据

import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
    
    const [count, setCout] = useState(0);
    const ref = useRef<number>()

    useEffect(() => {
        console.log('执行useEffect')
        ref.current = count
    })
    
    return(
        <div>
            <div>前一个count的值为:{ref.current}</div>
            <button onClick={() => setCout(count + 1) }>自增按钮</button>            
        </div>
    )
}
export default Demo

原理:因为组件渲染后才执行,因此渲染 <div>前一个count的值为:{ref.current}</div> 时,拿到的 ref.current 是未执行 useEffect 方法时的数据


仅在挂载和卸载的时候执行

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组 [] 作为第二个参数,相当于 effect 不依赖于 propsstate 中的任何值,所以它永远都不需要重复执行

const [value, setValue] = useState(xxx);
 
useEffect(() => {
  	...
}, []) 	// 不会在更新组件时执行

使用useEffect 获取数据并显示

在类组件中,一般通过将此代码放在 componentDidMount 方法中实现,函数式组件中,可以使用 useEffect hook 来实现

// 从Reddit网站获取帖子并显示它们的组件
import React, {useEffect, useState} from 'react'

const LifecycleDemo = () => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        // 在useEffect使用异步时应该这么写,即声明后立即调用,不能只声明不调用
        (async () => {
            const res = await fetch("https://www.reddit.com/r/reactjs.json")
            const json = await res.json();
            setPosts(json.data.children.map(c => c.data));
        })()
    })

    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    );
}

export default LifecycleDemo

控制获取文章的更新

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

interface IHelloProps {
    subreddit: string
}

// 假设从上级获取到请求的具体的路径
const LifecycleDemo:React.FC<IHelloProps> = ({subreddit}) => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        (async () => {
            const res = await fetch(`https://www.reddit.com/r/${subreddit}.json`)
            const json = await res.json();
            setPosts(json.data.children.map(c => c.data));
        })()
    }, [subreddit])		// 每次subreddit发生更改时从新获取文章,否则不会执行effect方法重新获取文章
 
    return (
        <ul>
          {posts.map(post => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
}

export default LifecycleDemo

useRef

作用:

它像一个变量, 类似于 this , 可以存放任何东西,和 createRef 一样可用来获取标签的 dom,也可以用来保存跨周期的数据信息

// 点击按钮 state 发生变化,然后每次重新渲染 Demo 组件时, 又会执行一遍 `let num = 0`,num 又会被重新赋值为 0,因此 num 会一直为 0,即一般在组件内无法跨周期进行保存
import React, { useState, useEffect } from 'react';
const Demo: React.FC = () => {
    const [numState, setNumState] = useState(0)
    let num = 0
    function change() {
        num += 1
        setNumState(numState + 1)
    }

    console.log(num)		// 始终显示0
    console.log(numState)	// 会持续自增 

    return(
        <div>           
            <button onClick={change}></button>
        </div>
    )
}
export default Demo

使用:

//在ts中使用useRef方法需要指定类型,否则会报错
const ref = useRef(null)	// 方法一:指定默认值,编译器自动类型推论指定类型
const ref = useRef<any>()	// 方法二:指定具体泛型类型
// 用useRef创建了couterRef对象,并将其赋给了button的ref属性,这样就能通过访问couterRef.current就可以访问到button对应的DOM对象
import React, {useRef} from 'react'

const eve =(ref:any) => {
    return () => {
        ref.current.disabled =true
    }
    
}

const Demo = () => {
    const buttonRef = useRef(null)	// 注意,如果这里是使用typescript来写的话要传入一个null值,否则类型报错
    
    return (
            <button ref={buttonRef} onClick={eve(buttonRef)}>button</button>
    )
}
export default Demo

与createRef的区别:

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用,由于这个特性,因此修改它的值不会触发重新渲染

即:useRef.current 一旦被赋值,在更新渲染时,这个值不会被消除,但是 createRef.current 会在更新阶段时重新渲染并重新绑定值

import React, { useState, useRef, createRef } from 'react';
const Demo = () => {
    const [renderIndex, setRenderIndex] = useState(1)
    const refFromUseRef = useRef<any>()
    const refFromCreateRef = createRef<any>()
    
    // 测试每次更新渲染是否会丢失其useRef.current值
    if(!refFromUseRef.current) {
        console.log('useRef被渲染了')   
    }
    
    // 测试每次更新渲染是否会丢失其createRef.current值
    if(!refFromCreateRef.current) {
        console.log('createRef被渲染了')
    }
    
    return(
        <>
            <div ref={refFromCreateRef}>测试createRef</div>
            <div ref={refFromUseRef}>测试UseRef</div>
        
            {/* 通过更改state值让组件更新数据重新渲染 */}
            <button onClick={()=> setRenderIndex(renderIndex + 1)}>测试按钮</button>
        </>
    )

}
export default Demo

// 输出结果:
// 首次渲染时会打印 `useRef被渲染了`和 `createRef被渲染了` 
// 更新渲染时只会打印 `createRef被渲染了`

useRef.current 的值可以被赋予任意的值而不是一定是dom,createRef.current 的值仅在通过标签 ref 获得,且是只读的,不可更改

const refFromUseRef = useRef<any>()
const refFromCreateRef = createRef<any>()

refFromUseRef.current = 'jack'		// 成立
refFromCreateRef.current = 'mary'	// 报错

因此 useRef 的特性为:可以存储数据,且不会在更新渲染时丢失这些数据


使用场景:

一:

import React, { useState } from 'react';
const Demo = () => {
    const [count, setCout] = useState(0);

    function handleAlertClick() {
        setTimeout(() => {
            alert(count)
        },3000)
    }
    
    return(
        <div>
            <div>当前count的值为:{count}</div>
            <button onClick={() => setCout(count + 1) }>自增按钮</button>
            <button onClick={handleAlertClick}>弹窗按钮</button>
        </div>
    )
}
export default Demo
  • 代码功能:点击自增按钮实现 count 增加,点击弹窗按钮延时3秒显示当前的 count
  • 存在问题:当点击弹窗按钮后不再次点击自增按钮时,延时后能显示和当前 count 一样的数值,但是当点击弹窗按钮后再继续点击自增按钮时,延时后弹窗的数值和当前的 count 是不能对应上的
  • 原因:React 会重新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并重新渲染一个 handleAlertClick 函数. 每一个 handleAlertClick 里面都有它自己的 count

可以理解为:按钮事件在点击时就已经确定,开始事件时会将当前生命周期(发生更新渲染前)的state和其他数据传入(如果有用到这些数据)

如果不使用 state 而使用一个普通的变量来进行传值,是可以获取到实时的数值的,原因是事件拿到的是当前生命周期的数据,而不触发更新渲染时,延时完执行时也能做出拿到当前的数据值,如下代码所示:

import React, { useState } from 'react';
const Demo = () => {
    let count = 0
    function handleAlertClick() {
        setTimeout(() => {
            alert(count)
        },3000)
    }
    
    function changeNum() {
        count += 1
        console.log(count)
    } 

    return(
        <div>
            <button onClick={changeNum}>自增按钮</button>
            <button onClick={handleAlertClick}>弹窗按钮</button>
        </div>
    )
}
export default Demo

使用 useRef 解决问题:

import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
    
    const [count, setCout] = useState(0);
    const latestCount = useRef(count)

    useEffect(() => {
        latestCount.current = count
    })

    function handleAlertClick() {
        setTimeout(() => {
            alert(latestCount.current)
        },3000)
    }
    
    return(
        <div>
            <div>当前count的值为:{count}</div>
            <button onClick={() => setCout(count + 1) }>自增按钮</button>
            <button onClick={handleAlertClick}>弹窗按钮</button>
        </div>
    )

}
export default Demo

注意的是如果state是一个对象,事件使用的是这个对象里的数据,在触发事件时传递的只是这个 state 的引用地址,而非对象内部具体的值,相当于只传 state 的栈数据不传堆数据

useRef 每次都会返回同一个引用,所以每一次渲染点击事件拿到的 state 都是一个引用地址,等到需要取数据的时候再从这个引用地址里去取数据,因此它可以达到实时的效果

这里能获取到当前count值而非上一个原因是点击事件执行时,异步useEffect函数已完成count的赋值

可用引用类型的方式写:功能是一样的,也能获取到实时的数据,原理一样

import React, { useState } from 'react';
const Demo = () => {
    const [obj, setCout] = useState({count:0});
    const [num, setNum] = useState(0)

    function handleAlertClick() {
        setTimeout(() => {
            alert(obj.count)
        },3000)
    }
	
    // 点击自增按钮同时更改obj.count 和 num
    function changeState(){
        setCout(() => {
            obj.count += 1
            return obj
        })
        setNum(num + 1)
    }
    
    return(
        <div>
            <div>当前count的值为:{num}</div>
            <button onClick={changeState}>自增按钮</button>
            <button onClick={handleAlertClick}>弹窗按钮</button>
        </div>
    )
}
export default Demo

二:

渲染周期之间共享数据的存储

state不能存储跨渲染周期的组件,因为state的参数每一次保存都会触发组件的重渲染

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

    // 把定时器设置成全局变量使用useRef挂载到current上
    const timer = useRef<any>();

    // 首次加载useEffect方法执行一次设置定时器
    useEffect(() => {
      timer.current = setInterval(() => {
        setCount(count => count + 1);
      }, 1000);
    }, []);

    // count每次更新都会执行这个副作用,当count > 5时,清除定时器
    useEffect(() => {
      if (count > 5) {
        clearInterval(timer.current);
      }
    });

    return <h1>count: {count}</h1>;
}
export default Demo

useReducer

引入 Reducer 功能

const [state, dispatch] = useReducer(reducer, initialState);
import React, {useReducer} from 'react'

// reducer
const myReducer = (state, action) => {
  switch(action.type)  {
    case('countUp'):
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}

// 组件
function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}

useContext

和 Context 差不多,还是要使用 createContext()

createContext()返回一个对象,对象里有Provider和Consumer两个属性,前者用于提供状态,后者用于接收状态
可传递一个参数作为默认的Provider值,当向上层组件找不到Provider值时则使用默认值

useContext()接收的参数为 createContext() 返回的对象,该方法的返回值为 Provider 传递的 value 值

// App.tsx
import React, { createContext } from 'react';
import Demo from './Demo'

interface ITemeProps  {
    [key: string]: {color: string; background: string;}
}

const themes: ITemeProps = {
    light: {
        color: '#000',
        background: '#eee'
    },
    dark: {
        color: '#fff',
        background: '#222'
    }
}

export const ThemeContext = createContext(themes.light)

const App: React.FC = () => {
    return (
        <div>
            {/* 使用Provider的value传递值,需要将要传递数据的组件包在内 */}
            {/* 子组件使用数据时,只需要useContext()去接受createContext()返回的对象便可拿到传递的value值 */}
            <ThemeContext.Provider value={themes.light} >
                <Demo />
            </ThemeContext.Provider>
        </div>
    )
}

export default App
// Demo.tsx
import React, { useState, useEffect, useContext} from 'react';
import {ThemeContext} from './App'

const Demo:React.FC = () => {
    const theme = useContext(ThemeContext)
    // 拿到传递的value值
    const style = {
        background: theme.background,
        color: theme.color
    }
    return <div style={style}></div>
}
export default Demo

自定义Hook

复用逻辑代码

  • 是一个函数式组件,使用 use 开头命名表示是一个 hook
  • 返回值不是 html 标签,而是给其他组件进行使用的数据
import { useState, useEffect } from 'react';
const useMousePostion  = () => {
    const [positions, setPositions] = useState({x:0, y:0})

    useEffect(() => {
        const updataMouse = (e: MouseEvent) => {
            console.log('inner')	
            setPositions({x: e.clientX, y: e.clientY})
        }
        document.addEventListener('click', updataMouse)
        return () => {
            document.removeEventListener('click', updataMouse)
        }
    },[])
   
    return positions
}
export default useMousePostion
import useMousePostion from './useMousePostion'
const App:React.FC = () => {
    const positions = useMousePostion()
    
    return(
    	<p>
        	{positions.x, positions.y}
        </p>
    )
}

hook解决的问题

可用 hook 去代替高阶组件去为组件添加要多次使用的额外功能

高阶组件缺点:代码沉重(如每次需要返回新的无用 html 标签);代码可读性不高等

例子

设置一个可复用的请求 loading 功能,在请求开始时设置 loading 状态为 true,

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

const useURLLoader = (url: string, deps: any[] = []) => {
    const [data, setData] = useState<any>(null)
    const [loading, setLoading] = useState(false)
    useEffect(() => {
        setLoading(true)
        axios.get(url).then(result => {
            setData(result.data)
            setLoading(false)
        })
    }, deps)
    return [data, loading]
}
export default useURLLoader

给组件复用:不像高阶函数那样需要给组件套方法导出,直接拿过来用即可

import useURLLoader from './useURLLoader'
const App: React.FC = () => {
    const [isRequest, setIsRequest] = useState(false) 
    const [data, loading] = useURLLoader('https://www.baidu.com', [isRequest]) 
    
    return (
        <div>
            <button onclick={() => setIsRequest(!isRequest)}>点击重新请求数据</button>
            {
                loading
                    ?
                <div>获取的数据为{data}</div>
                    :
                <div>Loading...</div>
            }
       	</divdiv>
    )
}

Hook使用规则
  • 只在最顶层使用Hook
    即不在条件,循环,函数嵌套中使用hook,确保hook在函数组件的最顶层调用,这样能确保hook在渲染中能按相同的顺序调用
  • 只在 React 函数中调用 Hook