React + TypeScript实战(二)hooks用法

本文采用的react相关技术为:

一、函数式组建的声明方式

import react, { FC } from 'react'

type IProps = {
    message: string
}

const Test: FC = (props) => {
  return 
{props.message}
} export default Test
  • React.FC(可以直接写为FC)显式地定义了返回类型,其他方式是隐式推导的
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全

如果没有手动为FC声明类型,则会报类型错误,此时我们需要手动进行类型的二次转换,比如:

// wrong
const Test: React.FC<{}> = () => 'hello'

// correct
onst Test: React.FC<{}> = () => ('hello' as unknown) as JSX.Element

二、useState

useState()接受一个参数为默认值,该方法返回一个数组,第一个值为定义data的值,第二个为更新data的方法,他们总是成对出现的:

import react, { FC, useState } from 'react'

type IProps = {
    message?: string
}

const Test: FC = (props) => {
  // 大部分情况下,TS 会自动为你推导 state 的类型
  // 这里会自动将 name 推导为 string 类型
  let [name, setName] = useState("Ryuko_黑猫几绛")

  return (
    
) } export default Test

2.1 useState使用注意点

2.1.1 useState 是异步的

修改state之后无法拿到最新的状态,要等到下一个事件循环周期执行时,状态才是最新的:

const Test: FC = props => {
  const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })

  const handleUpdate = () => {
    setPeople({ ...people, name: '王五' });
    console.log(people.name); // 张三
  }

  return (
    
{ // dom 上面的数据会改变为 '王五' } {people.name}
) } export default Test;

但是在state不影响DOM的前提下,你是可以同步使用它:

const Test: FC = props => {
  const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })

  const handleUpdate = () => {
    setPeople({ ...people, name: '王五' });
    people.name = "王五";
    console.log(people.name); // 王五
  }

  return (
    
{ // dom 上面的数据会改变为 '王五' } {people.name}
) } export default Test;

2.1.2 useState 根据地址判断更新

useState要触发页面的更新,是比较新的值和旧的值是否一致(对于引用类型而言,比较是新旧对象的内存地址是否一致),如果不一致才会更新页面,所以若两次传入同一对象则不会触发组件更新

2.1.3 useState 将新值直接覆盖掉旧值,而不是合并

const [temp,setTemp] = useState({a: 1, b: 2});
setTemp({a: 2}); // temp = {a: 2}

因此,如果是处理复杂的对象数据,我们可以这样做:

// 同样含义的变量可以合并成一个 state,代码可读性会提升很多
const [userInfo, setUserInfo] = useState({
  firstName,
  lastName,
  school,
  age,
  address
})

setUserInfo(s=> ({
  ...s,
  fristName,
}))

三、useRef

const refContainer = useRef(initialValue);

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

一个常见的用例便是命令式地访问子组件:

const TextInputWithFocusButton: FC = () => {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      
      
    
  );
}

注意:无法直接通过ref来引用函数组件,因为函数组件没有对象

const ref = useRef(null)

// wrong!


// correct!

不过,我们可以通过父子组件通信的思想:父组件转发自己的ref给子组件,然后在父组件中通过这个ref(传递过去后会被子组件绑定)操作子组件的dom

通过查文档可知,如果想实现上述的引用思想,需要为子组件函数声明为:ForwardRef类型:

function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

可以看到这个方法需要接收一个ForwardRefRenderFunction的函数参数,这个函数的声明为:

interface ForwardRefRenderFunction<T, P = {}> {
    (props: P, ref: ForwardedRef<T>): ReactElement | null;
    displayName?: string | undefined;
    // explicit rejected with `never` required due to
    // https://github.com/microsoft/TypeScript/issues/36826
    /**
         * defaultProps are not supported on render functions
         */
    defaultProps?: never | undefined;
    /**
         * propTypes are not supported on render functions
         */
    propTypes?: never | undefined;
}

这是一个函数式接口,总的来看,我们需要为forwardRef提供一个函数参数,这个参数需要:

  • 参数值
    • props: P
    • ref:ForwardedRef
  • 返回值:ReactElement | null

因此我们可以这样声明子组件:

import react, { FC, forwardRef } from 'react'

/**
 *  @params props 父组件传递进来的数据
 *  @params ref 在父组件中定义的ref,交给子组件来绑定
 */
const Son = forwardRef((props,ref)=>{
  return (
    <>
      
    
  )
})

export default Son

现在子组件目标元素绑定了父组件传递进来的ref数据,我们可以回到父组件直接进行操作了:

import react, { FC, useRef } from 'react'
import Son from './Son'

const Father: FC = () => {
  const inputRef = useRef(null)
  return (
    <>
      {
      	// 给子组件绑定 ref 
      }
      
      
    
  )
}

export default Father

3.1 比 ref 更有用

从前面的例子来看,useRef似乎和ref相同,都是获取到实例dom节点

注意这一句话:ref 对象在组件的整个生命周期内持续存在并且保持不变,因此当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。

根据useRef的这个性质,我们可以用来模拟实现全局变量

下面看看这个例子,需求是点击按钮让点赞数 + 1,然后点击Alert弹框显示当前点赞数

import React, { useState } from "react";
const LikeButton: React.FC = () => {
    const [like, setLike] = useState(0)
    function handleAlertClick() {
        setTimeout(() => {
            alert(`you clicked on ${like}`) 
        }, 3000)
    }
    return (
        <>
            
            
        
    )
}
export default LikeButton

上面的代码看起来没有问题,但是由于在handleAlertClick中形成了闭包,所以弹出来的是第一次触发函数时的like值。也就是说,当我点赞数为5时,点击Alert,然后迅速点赞到10,最终弹出的值为5。

为什么不是界面上like的实时状态?
当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。

总结:不同渲染之间无法共享state状态值

3.1.1 采用全局变量

在组件前定义一个类似 global 的变量

import React from "react";
let like = 0;
const LikeButton: FC = () => {
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like}`);
    }, 3000);
  }
  return (
    <>
      
      
    
  );
};
export default LikeButton;

总结:由于like变量是定义在组件外,所以不同渲染间是可以共用该变量,所以3秒后获取的like值就是最新的like值,该示例同时也说明:非state变量不会引起重新render

3.1.2 采用 useRef

import React, { useRef } from "react";
const LikeButton: FC = () => {
  // 定义一个实例变量
  let like = useRef(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like.current}`);
    }, 3000);
  }
  return (
    <>
      
      
    
  );
};
export default LikeButton;

总结:由于 useRef更改不会re-render,所以用useRef 作为组件实例的变量,保证多个不同渲染过程中,获取到的数据肯定是最新的。

3.2 useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时给父组件暴露的指定的值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

以这一章最开始的例子来说,还是先从子组件入手:

import react, { FC, forwardRef, useImperativeHandle } from 'react'

const Son = forwardRef((props,ref)=>{
  useImperativeHandle(ref, ()=>({
    talk: ()=>{
      console.log("hi Ryuko!");
    }
  }))
  return (
    <>
      
    
  )
})

export default Son

此时可以在父组件中直接调用子组件内部的方法:

import react, { FC, useRef } from 'react'
import Son from './Son'

export interface InputRefProps{
  talk(): void
}

const Father: FC = () => {
  const inputRef = useRef(null)
  return (
    <>
      
      
    
  )
}

export default Father

四、useEffect

首先看看这个hook的函数定义:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

在useRef这一章的例子中我们知道了,funcion component每次Render的内容都会形成一个快照并保存下来,因此当状态变更而re-render时,会形成N个Render状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。即:函数在每次渲染时是独立的

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

4.1 每次 Render 都有自己的 Effects

useEffect是react提供的一个专门来帮我们处理副作用的钩子

useEffect 在实际 DOM 渲染完毕后执行,它每次渲染时对应的数据也是独立的

虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // Our deps

直到 name 改变时的 Rerender,useEffect 才会再次执行。

  • 如果我们没有给useEffect第二个参数,回调函数会在每次第一次创建组件componentDidMount和组件数据改变componentDidUpdate时执行
  • 如果第二个参数为 [ ],则只在第一次创建组件时执行

4.2 清除副作用

4.2.1 无需清理的副作用

有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。

需求:监听url的变化来发送网络请求,保存返回结果

import React, { FC, useState, useEffect } from "react";
import ajax from '@utils/ajax'

type IProps = {
  location:string
}

const Example: FC = (props) => {
  const [data, setData] = useState({});

  useEffect(() => {
    getData();
  }, [props.location]);

  const getData = () => {
    ajax.post().then(res => {
      setData(res);
    })
  }
  return 
{data}
} export default Example;

当location发生变化时,useEffect中函数就会自动执行。

4.2.2 需要清理的副作用

const timer = useRef()

//componentDidMount 和 componentDidUpdate
//可以在useEffect中来处理副作用
useEffect(() => {
    console.log("useEffect执行了")
    //副作用可以分为两类:
    //1.无需清理的副作用: 发送网络请求,获取服务器相应数据,修改state
    //2.需要清理的副作用: 订阅服务 定时器 ,只需要在useEffect中返回一个函数就可以在这个函数里面清除副作用
    timer.current = setInterval(()=>{
        console.log("定时器执行了")
    },1000)

    //componentWillUnmount
    //这边返回的清除函数的执行时机:是在下一次函数组件re-render之后,useEffect之前执行
    return () => {
        console.log("清除函数执行了")
        clearInterval(timer.current)
    }
}

4.3 useLayoutEffect 和 useInsertionEffect

useEffect和useLayoutEffect的异同:

  • useLayoutEffect是在虚拟DOM构建完成后立即执行,useEffect是在真实DOM构建完成后立即执行

  • useLayoutEffect是同步执行,useEffect是异步执行

useInsertionEffect可以向页面中插入dom元素
useLayoutEffect可以在绘制屏幕前修改dom元素的样式

React + TypeScript实战(二)hooks用法_第1张图片

五、useMemo & useCallback

首先看看这个hook的函数定义:

function useMemo(factory: () => T, deps: DependencyList | undefined): T;

useMemo的泛型显示的制定了参数的返回类型,例如:

// wrong!
// 不能将类型“number”分配给类型“string”。ts(2322)
const result = React.useMemo<string>(() => 1, [])

useMemo类似于Vue的计算属性,如果有一些属性值是可以根据其他值推导出来的,我们就可以使用useMemo

它的参数有两个:

  • 一个函数,函数的返回值就是useMemo的结果
  • 数组依赖项,表示触发第一个函数参数的条件
const length = useMemo(() => {
    return list.length;
}, [list]);

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

//double依赖于count,当count改变时,double自动改变
let double = useMemo(() => {
    return count * 2
}, [count]);   

5.1 缓存变量

回顾一下,函数组件什么时候发生重新渲染:

1. 组件重新被创建
2. 当前组件state的内存地址发生了变化,无论dom中是否使用了state
3. 父组件更新,子组件也会自动的更新
4. 组件更新时,会卸载所有function,并重新创建function (执行函数组件的所有逻辑)

注意第三点,父组件更新,子组件也会自动的更新

这意味着,如果父组件有变量a,b,并将b传递给了子组件;如果修改了a的值,会触发父组件的更新,此时子组件也会自动更新,例如:

// 父组件
import React, { useState, useMemo } from "react";
import Son from "./components/Son";

function Father() {
  const [count, setCount] = useState(100);
  //useMemo()可以实现类似于Vue中的计算属性的功能,还可以用来缓存数据
  const obj = {
    name: "zhangsan",
  };
    
  //下面的obj2在函数组件每一次重新渲染的时候都是同一个对象,没有重新初始化
  const obj2 = useMemo(() => {
    return {
      name: "zhangsan",
    };
  }, []);

  return (
    
{ // 点击按钮以后,由于函数会重新执行,父组件中将创建一个新的obj,传递给子组件的obj也会改变 // 因此会触发子组件的自动更新 } { // 不会触发更新 }
); } export default Father;

memo() 是一个高阶组件,高阶组件其实就是一个函数,只不过这个函数的参数是一个组件,函数的返回值是一个新的组件

memo()可以实现类似于类组件的React.PureComponent功能

只要父亲传递给孩子的props发生了变化就应该刷新子组件(如果父亲没有给子组件传递props或者父亲给子组件传递的props没有改变,则子组件不应该刷新

父组件如果使用了useMemo,子组件一定要配套使用memo函数

import React, { FC, useState, memo } from "react";

type IProps = Readonly<{
  obj?: {
    name: string;
  };
}>;

const Son: FC = () => {
  const [message, setMessage] = useState("hello");
  return (
    
Son
); }; export default memo(Son);

5.2 缓存函数

在下面这个例子中,父组件将一个函数传给了子组件,子组件通过props.onChange,将数据传递回父组件,然后父组件修改text数据:

// Father.tsx
import react, { FC, useState } from 'react'
import Son from './Son'

export default const Father: FC<{}> = () => {
  const [text,setText] = useState("")
  const changeHandler = (event:React.ChangeEvent)=>{
    setText(event.target.value)
  }
  return (
    <>
      
text文本为:{text}
) } // Son.tsx import react, { FC, forwardRef, useImperativeHandle } from 'react' export default const Son: FC<{ onChange?: (event: React.ChangeEvent) => void; }> = memo((props) => { return ( <> ) })

但是,每一次子组件输入内容,父组件的text文本发生变化,造成父组件被重新渲染,从而造成父组件传递给子组件的changeHandler方法(props)发生了变化,从而造成子组件的重新渲染。

此时我们可以通过useCallback来解决这个问题:

const changeHandler = useCallback((event:React.ChangeEvent)=>{
    setText(event.target.value)
},[])

5.3 区别和联系

实际上useCallback是基于useMemo实现的,useMemo是返回callback执行后的结果

function useCallback(callback, args) {
	return useMemo(() => callback, args);
}

六、useContext

跨级组件通信,实现同一子树下所有节点可统一共享子树根节点的数据

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

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

基本用法:

#1.src/context/index.ts
import raect, { createContext, Context } from 'react'

type ContextType = {
  name: string,
  age: number
}

let context: Context  = createContext(null)

// 暴露出来一个 context 对象
export default context


#2 index.tsx
import Context from './context/index'
root.render(
  
  	
  
)

#3 components/Son.tsx
import Context from '../context/index'

const Son: FC<{}> = (props) => {
  // 在子组件中获取根组件暴露的数据
  const contextValue = useContext(MyContext);
  return (
    
text文本为:{contextValue.name}
) }; export default Son

七、useReducer

首先看看这个hook的函数定义:

function useReducer>(
    reducer: R,
    initialState: ReducerState,
    initializer?: undefined
): [ReducerState, Dispatch>];

必要的参数有:

  • 一个继承自Reducer类型的reducer纯函数
  • 初始化state状态
  • 可选参数init,负责惰性计算state初始值

返回值为一个元组,表示为[state, dispath分发函数]

接下来看看reducer纯函数的定义,需要初始值以及action:

 type Reducer = (prevState: S, action: A) => S;

所以,useReducer是在函数组件中实现类似 Redux 功能的一个Hook。他接收两个参数,第一个参数是一个recuder(纯函数),第二个参数是state的初始值。

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

注意

import React, { useState, useReducer } from "react";

type StateType = Readonly<{
  count: number;
}>;

//创建一个纯函数
const reducer = (state: StateType, action: any) => {
  switch (action.type) {
    case "ADD":
      return { count: state.count + 1 };
    case "SUB":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

function App() {
  // 使用useReducer(纯函数) ,得到state和dispatch
  const [state, dispatch] = useReducer(reducer, { count: 1000 });
  return (
    
{state.count}
); } export default App;

八、useReduce & useContext 实现全局数据共享

在六、七章中不难发现,我们可以创建一个全局的Context对象,在这个对象中放入全局数据,比如:

import react, { createContext } from 'react'

let context = createContext({
    name: 'Ryuko_黑猫几绛',
    age: 18
})

export default context

然后父组件采用的形式将数据暴露给后代,在子组件中只需要引入这个context,然后通过useState将对象值取出就可以使用了。

如果想要更改这样的全局变量,第一反应是通过某个函数来触发更改。useReduce恰好提供了这样的功能。

总的来说:

  • useContext负责向子孙组件暴露数据
  • useReducer提供全局的state、reducers、dispatch等

8.1 创建全局context

首先回顾useReduce的函数定义:

function useReducer<R extends Reducer<any, any>>(
    reducer: R,
    initialState: ReducerState<R>,
    initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

// Dispatch>其实就是一个函数,这个函数接收一个参数泛型 A
type Dispatch<A> = (value: A) => void;

他可以返回一个包裹着statedispatch函数的元组,这个函数接收ReducerAction类型的泛型。

凭印象大体写出这样的代码:

import react, { createContext } from 'react'

let context = createContext()

export default context

通过查看createContext的函数定义发现,这个方法需要传入一个参数表示默认变量值,同时这个方法接收一个泛型。

现在想想,我们需要使用到什么样的泛型定义呢?

为了满足useReducer的元组返回类型,首先需要一个全局state表示仓库的管理,同时还需要一个action表示对数据的处理操作:

import react, { createContext, Dispatch } from 'react'

export type StateType = {
  userInfo: {
    name: string,
    avater: string
  },
  position: string
}

// 声明Action的泛型
export type ActionType = {
  type: string,
  payload: any
}

export type ContextType = [
  StateType,
  Dispatch
]

// 可以把这里的 context 当作一个仓库,
let context = createContext([
    {position:""},()=>{}
])

export const reducer = (state: StateType, action: ActionType) => {
  switch (action.type) {
    case "LOACTION":
      return { ...state, position: action.payload.position }
    // 切记 需要返回默认值
    default:
      return state
  }
}

export default context

现在我们到根目录的index.tsx文件中进行配置。

也许你会想说,在这里直接引入Context全局对象,使用useReducer计算出state和dispath,然后通过context.provider向子组件传递公共value={[state, dispatch]}就可以了。但需要注意的是,react hook 只能执行在函数式组件中:

// ...
import MyRedux from './components/MyRedux'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
 
)

8.2 reducer内容分发

所以我们创建一个叫做MyRedux的对象,将这个对象作为中转容器,然后向他的子组件传递全局中的数据

import react, { FC, useReducer } from 'react'
import MyContext, { reducer } from '../context'
import Son from './Son.tsx'

const MyRedux: FC<{}> = () => {
  const [state, dispatch] = useReducer(reducer, {position:""})
  return (
    <>
      
        
      
    
  )
}

export default MyRedux

现在内容分发给了子组件,在组组件中直接通过useContext注册属性以及方法即可:

import react, { FC, useContext } from 'react'
import context from '../context'

const Son: FC<{}> = () => {
  const [state, dispatch] = useContext(context)
  return (
    <>
      
{JSON.stringify(state)}
) } export default Son

九、自定义 Hooks

import React, { useState, useCallback, useEffect } from "react";

export const useWinSize = () => {
  // 1. 使用useState初始化窗口大小state
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });

  const changeSize = useCallback(() => {
    // useCallback 将函数缓存起来 节流
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  }, []);
  // 2. 使用useEffect在组件创建时监听resize事件,resize时重新设置state (使用useCallback节流)
  useEffect(() => {
    //绑定一次页面监听事件 组件销毁时解绑
    window.addEventListener("resize", changeSize);
    return () => {
      window.removeEventListener("resize", changeSize);
    };
  }, []);

  return size;
};

参考文章

  • useRef详细总结
  • 精读《useEffect完全指南》
  • 五个大型项目实践总结,解密React Hooks最佳实践

你可能感兴趣的:(React,react.js,typescript,javascript)