React hooks的用法

447 阅读5分钟

类组件是一种面向对象思想的体现,类组件之间的状态会随着功能增强而变得越来越臃肿,代码维护成本也比较高,而且不利于后期 tree shaking。所以有必要做出一套函数组件代替类组件的方案,于是 Hooks 也就理所当然的诞生了。

hooks诞生的原因

  • 组件之间的状态复用困难
  • 类组件理解成本较高,业务逻辑分散在生命周期中
  • class和this的特性较为复杂
  • 解决函数组件无状态只能用于简单组件的痛点,使其拥有持续化的状态
  • 拥抱函数式编程

使用hook注意事项

  • 只在只在最顶层使用Hook
  • 不要在循环, 条件或嵌套函数中调用 Hook

常用hooks

1. useState

解决函数组件无状态只能用于简单组件的痛点,使其拥有持续化的状态。

  • 用法
const [value, setValue] = useState(initValue);

setValue(newValue);
setValue((value) => newValue);
  • 注意
    • 可以在同一个函数组件中被调用多次
    • 在hook中,state状态值不再要求一定是一个对象,可以是数字等简单类型
    • state的类型是对象时,不会自动合并
    • 无回调函数
    • 多个不同的state,为了更好的复用,建议使用多个useState

2. useEffect

副作用函数,在依赖的数据发生变化时执行,可用来监听状态变化,模拟生命周期等

  • 用法
useEffect(() => {
    // 依赖发生变化了...

    return () => {
        // 下一次useEffect运行前被执行
    };
}, [依赖的状态]);
  • 注意

    • useEffect可以在同一个函数组件中被调用多次
    • 第一个参数是函数,返回值函数在下一次执行前执行
    • 第二个参数是数组,可以指定绑定的关联数据,不传每次更新都会执行,为空数组则只执行一次
    • useEffect不能被打断
    • useEffect不能被判断包裹
    • 可用useEffect模拟生命周期
  • 模拟生命周期

useEffect(() => {
    // componentsWillUpdate
    // componentMounted (仅在依赖数组为空时)
    return () => {
        // componentWillUnmount (仅在依赖数组为空时)
        // 这里常用来解绑事件,取消订阅
    };
}, [依赖的状态]);
  • useEffect 里面使用到的state的值, 固定在了useEffect内部,不会被改变,除非useEffect刷新,重新固定state的值。在依赖中添加count即可触发刷新
 const [count, setCount] = useState(0)
useEffect(() => {
    console.log('use effect...',count)
    const timer = setInterval(() => {
        console.log('timer...count:', count) // 一直是
        setCount(count + 1)
    }, 1000)
    return ()=> clearInterval(timer)
},[])

3. useRef

希望可以实时拿到最新的值,并且不需要添加到依赖不会触发useEffect重新执行,可用来获取dom等。

useEffect 里面使用到的state的值, 固定在了useEffect内部, 不会被改变,除非useEffect刷新,重新固定state的值

  • 用法

import React, { useRef, forwardRef, useImperativeHandle } from 'react'

// 转发透传ref
const ForwardRefIndex = React.forwardRef((props, ref) => <Input  {...props} ref={ref}  />)

function Home(){
    const ref = useRef(null)
     useEffect(() => {
         console.log(ref.current)
     }, [])
    return <Input ref={ref} />;
}


// 父组件调用子函数组件的方法
const JMInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    },
  }));
  return <input type="text" ref={inputRef} />;
})

function ImperativeHandleDemo() {
  const inputRef = useRef();
  return (
    <div>
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <JMInput ref={inputRef} />
    </div>
  )
}

4. useContext

解决通用状态在所有子组件中共享的问题,可以在所有子孙组件中使用状态

  • 使用
const ThemeContext = React.createContext(null)


// 消费者
const Son = () => {
  const { color, background } = useContext(ThemeContext);
  return <div style={{ color, background, padding: '20px' }}>这是子组件</div>;
};

// 提供者
const ThemeProvider = ThemeContext.Provider;
export default function ProviderDemo(){
    const [ contextValue , setContextValue ] = React.useState({  color:'#ccc', background:'pink' })
    return <div>
        <ThemeProvider value={ contextValue } > 
            <Son />
        </ThemeProvider>
    </div>
}

5. useMemo

使用缓存避免数据并未更新但是重新render减少渲染

  • 使用
const Child = memo(({data}) =>{
    console.log('child render...', data.name)
    return (
        <div>
            <div>child</div>
            <div>{data.name}</div>
        </div>
    );
})

const Hook = ()=>{
    console.log('Hook render...')
    const [count, setCount] = useState(0)
    const [name, setName] = useState('rose')
    const data = { name }
    return(
        <div>
            <div>{count}</div>
            <button onClick={()=>setCount(count+1)}>update count </button>
            <Child data={data}/>
        </div>
    )
}

当点击按钮更新count的时候,Effect组件会render,执行到const data = {name}这一行代码会生成有新的内存地址的对象,那么就算带着memo的Child组件,也会跟着重新render, 尽管最后其实Child使用到的值没有改变,重复render。

使用useMemo进行缓存,仅在name更新时再重新触发子组件渲染。

const data = useMemo(()=>{
   return { name };
},[name])

6. useCallback

useCallback 解决了函数未发生变化触发子组件更新的问题,

  • 注意

    • useMemo 是缓存值的
    • useCallback 是缓存函数的
  • 使用


const Hook = ()=>{
    console.log('Hook render...')
    const [count, setCount] = useState(0)
    const [name, setName] = useState('rose')
    const data = { name }
    const handleChange = (name) => setName(name);
    return(
        <div>
            <div>{count}</div>
            <button onClick={()=>setCount(count+1)}>update count </button>
            <Child data={data} onChange={handleChange}/>
        </div>
    )
}

7. useReducer

让函数式组件可以像类组件一样集中式管理状态。

  • 可以使用reducer的场景:
    • 如果你的state是一个数组或者对象
    • 如果你的state变化很复杂,经常一个操作需要修改很多state
    • 如果你希望构建自动化测试用例来保证程序的稳定性
    • 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
    • 如果你用应用程序比较大,希望UI和业务能够分开维护
const initState = {
  name: '',
  pwd: '',
  isLoading: false,
  error: '',
  isLoggedIn: false,
}
function loginReducer(state, action) {
  switch(action.type) {
      case 'login':
          return {
              ...state,
              isLoading: true,
              error: '',
          }
      case 'success':
          return {
              ...state,
              isLoggedIn: true,
              isLoading: false,
          }
      case 'error':
          return {
              ...state,
              error: action.payload.error,
              name: '',
              pwd: '',
              isLoading: false,
          }
      default: 
          return state;
  }
}
function LoginPage() {
  const [state, dispatch] = useReducer(loginReducer, initState);
  const { name, pwd, isLoading, error, isLoggedIn } = state;
  const login = (event) => {
      event.preventDefault();
      dispatch({ type: 'login' });
      login({ name, pwd })
          .then(() => {
              dispatch({ type: 'success' });
          })
          .catch((error) => {
              dispatch({
                  type: 'error'
                  payload: { error: error.message }
              });
          });
  }
  return ( 
      //  返回页面JSX Element
  )
}

自定义hook

使用react自带的基本hooks封装在业务中可复用的逻辑。

  • 使用useXXX 的命名规范
  • 遵循Hooks规则

1. useDebounceState:节流修改状态

import { useState, useMemo } from 'react';

export function debounce(fn, time) {
  let timer: any = null;
  return (...arg) => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, arg);
    }, time);
  };
}

export function useDebounceState(defaultValue, time = 300) {
  const [value, setValue] = useState(defaultValue);
  const handleChange = useMemo(() => debounce(setValue, time), [time]);
  return [value, handleChange];
}

// 使用
export default function Index(){
    const [ value , setValue ] = useDebounceState('', 300)
    return <div>
      {value}
      <input value={value} onChange={(e)=>setValue(e.target.value)}  />
    </div>
} 

2. useUserInfo:userId变化自动获取用户信息

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

const sleep = (time = 0) =>
  new Promise((resolve: any) => setTimeout(() => resolve(), time));

const fetchUserInfo = async (id: string) => {
  await sleep(400);
  return {
    id,
    name: 'alan',
    age: 18,
  };
};

export const useUserInfo = (id: string) => {
  const [userInfo, setUserInfo] = useState(null as any);
  const hashRef = useRef('');
  useEffect(() => {
    async function run() {
      const hash = Math.random().toString();
      hashRef.current = hash;
      const info = await fetchUserInfo(id);
      if (hashRef.current !== hash) {
        return;
      }
      setUserInfo(info);
    }
    run();
  }, [id]);
  return userInfo;
};