hooks和typescript使用
Posted 霍佳佳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了hooks和typescript使用相关的知识,希望对你有一定的参考价值。
课前预知
先启个项目~
npx create-react-app hook-ts-demo --template typescript
在 src/App.tsx
内引用我们的案例组件,在 src/example.tsx
写我们的案例组件。
函数式组件的使用~ 我们可以通过以下方式使用有类型约束的函数式组件:
import React from 'react'
type UserInfo = {
name: string,
age: number,
}
export const User = ({ name, age }: UserInfo) => {
return (
<div className="App">
<p>{ name }</p>
<p>{ age }</p>
</div>
)
}
const user = <User name='vortesnail' age={25} />
也可以通过以下方式使用有类型约束的函数式组件:
import React from 'react'
type UserInfo = {
name: string,
age: number,
}
export const User:React.FC<UserInfo> = ({ name, age }) => {
return (
<div className="User">
<p>{ name }</p>
<p>{ age }</p>
</div>
)
}
const user = <User name='vortesnail' age={25} />
上述代码中不同之处在于:
export const User = ({ name, age }: UserInfo) => {}
export const User:React.FC<UserInfo> = ({ name, age }) => {}
使用函数式组件时需要将组件申明为
React.FC
类型,也就是 Functional Component 的意思,另外props
需要申明各个参数的类型,然后通过泛型传递给React.FC
。
虽然两种方式都差不多,但我个人更喜欢使用 React.FC
的方式来创建我的有类型约束的函数式组件,它还支持 children
的传入,即使在我们的类型中并没有定义它:
export const User:React.FC<UserInfo> = ({ name, age, children }) => {
return (
<div className="User">
<p>{ name }</p>
<p>{ age }</p>
<div>
{ children }
</div>
</div>
)
}
const user = <User name='vortesnail' age={25}>I am children text!</User>
我们也并不需要把所有参数都显示地解构:
export const User:React.FC<UserInfo> = (props) => {
return (
<div className="User">
<p>{ props.name }</p>
<p>{ props.age }</p>
<div>
{ /* 仍可以拿到 children */ }
{ props.children }
</div>
</div>
)
}
const user = <User name='vortesnail' age={25}>I am children text!</User>
好了,我们暂时知道上面这么多,就可以开始使用我们的 hooks 了~
我将从三个点阐述如何结合 typescript
使用我们的 hooks
:
为啥使用❓
怎么使用
场景例举
useState
为啥使用useState?
可以让函数式组件拥有状态管理特性,类似 class 组件中的 this.state
和 this.setState
,但是更加简洁,不用频繁的使用 this
。
怎么使用useState?
const [count, setCount] = useState<number>(0)
场景举例
1.参数为基本类型时的常规使用:
import React, { useState } from 'react'
const Counter:React.FC<{ initial: number }> = ({ initial = 0 }) => {
const [count, setCount] = useState<number>(initial)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count+1)}>加</button>
<button onClick={() => setCount(count-1)}>减</button>
</div>
)
}
export default Counter
2.参数为对象类型时的使用:
import React, { useState } from 'react'
type ArticleInfo = {
title: string,
content: string
}
const Article:React.FC<ArticleInfo> = ({ title, content }) => {
const [article, setArticle] = useState<ArticleInfo>({ title, content })
return (
<div>
<p>Title: { article.title }</p>
<section>{ article.content }</section>
<button onClick={() => setArticle({
title: '下一篇',
content: '下一篇的内容',
})}>
下一篇
</button>
</div>
)
}
export default Article
在我们的参数为对象类型时,需要特别注意的是, setXxx
并不会像 this.setState
合并旧的状态,它是完全替代了旧的状态,所以我们要实现合并,可以这样写(虽然我们以上例子不需要):
setArticle({
title: '下一篇',
content: '下一篇的内容',
...article
})
useEffect
为啥使用useEffect?
你可以把 useEffect
看做 componentDidMount
, componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
怎么使用useEffect?
useEffect(() => {
...
return () => {...}
},[...])
场景举例
1.每当状态改变时,都要重新执行 useEffect
的逻辑:
import React, { useState, useEffect } from 'react'
let switchCount: number = 0
const User = () => {
const [name, setName] = useState<string>('')
useEffect(() => {
switchCount += 1
})
return (
<div>
<p>Current Name: { name }</p>
<p>switchCount: { switchCount }</p>
<button onClick={() => setName('Jack')}>Jack</button>
<button onClick={() => setName('Marry')}>Marry</button>
</div>
)
}
export default User
2.即使每次状态都改变,也只执行第一次 useEffect
的逻辑:
useEffect(() => {
switchCount += 1
}, [])
3.根据某个状态是否变化来决定要不要重新执行:
const [value, setValue] = useState<string>('I never change')
useEffect(() => {
switchCount += 1
}, [value])
因为 value
我们不会去任何地方改变它的值,所以在末尾加了 [value]
后, useEffect
内的逻辑也只会执行第一次,相当于在 class 组件中执行了 componentDidMount
,后续的 shouldComponentUpdate
返回全部是 false
。
4.组件卸载时处理一些内存问题,比如清除定时器、清除事件监听:
useEffect(() => {
const handler = () => {
document.title = Math.random().toString()
}
window.addEventListener('resize', handler)
return () => {
window.removeEventListener('resize', handler)
}
}, [])
useRef
为啥使用useRef?
它不仅仅是用来管理 DOM ref 的,它还相当于 this , 可以存放任何变量,很好的解决闭包带来的不方便性。
怎么使用useRef?
const [count, setCount] = useState<number>(0)
const countRef = useRef<number>(count)
场景举例
1.闭包问题:
想想看,我们先点击 加 按钮 3 次,再点 弹框显示 1次,再点 加 按钮 2 次,最终 alert
会是什么结果?
import React, { useState, useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState<number>(0)
const handleCount = () => {
setTimeout(() => {
alert('current count: ' + count)
}, 3000);
}
return (
<div>
<p>current count: { count }</p>
<button onClick={() => setCount(count + 1)}>加</button>
<button onClick={() => handleCount()}>弹框显示</button>
</div>
)
}
export default Counter
结果是弹框内容为 current count: 3 ,为什么?
当我们更新状态的时候, React 会重新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并重新渲染一个 handleCount 函数. 每一个 handleCount 里面都有它自己的 count 。
** 那如何显示最新的当前 count 呢?
const Counter = () => {
const [count, setCount] = useState<number>(0)
const countRef = useRef<number>(count)
useEffect(() => {
countRef.current = count
})
const handleCount = () => {
setTimeout(() => {
alert('current count: ' + countRef.current)
}, 3000);
}
//...
}
export default Counter
2.因为变更 .current
属性不会引发组件重新渲染,根据这个特性可以获取状态的前一个值:
const Counter = () => {
const [count, setCount] = useState<number>(0)
const preCountRef = useRef<number>(count)
useEffect(() => {
preCountRef.current = count
})
return (
<div>
<p>pre count: { preCountRef.current }</p>
<p>current count: { count }</p>
<button onClick={() => setCount(count + 1)}>加</button>
</div>
)
}
我们可以看到,显示的总是状态的前一个值:
3.操作 Dom 节点,类似 createRef():
import React, { useRef } from 'react'
const TextInput = () => {
const inputEl = useRef<htmlInputElement>(null)
const onFocusClick = () => {
if(inputEl && inputEl.current) {
inputEl.current.focus()
}
}
return (
<div>
<input type="text" ref={inputEl}/>
<button onClick={onFocusClick}>Focus the input</button>
</div>
)
}
export default TextInput
useMemo
为啥使用useMemo?
从 useEffect 可以知道,可以通过向其传递一些参数来影响某些函数的执行。React 检查这些参数是否已更改,并且只有在存在差异的情况下才会执行此。
useMemo 做类似的事情,假设有大量方法,并且只想在其参数更改时运行它们,而不是每次组件更新时都运行它们,那就可以使用 useMemo 来进行性能优化。
记住,传入
useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect
的适用范畴,而不是useMemo
。
怎么使用useMemo?
function changeName(name) {
return name + '给name做点操作返回新name'
}
const newName = useMemo(() => {
return changeName(name)
}, [name])
场景举例
1.常规使用,避免重复执行没必要的方法:
我们先来看一个很简单的例子,以下是还未使用 useMemo
的代码:
import React, { useState, useMemo } from 'react'
// 父组件
const Example = () => {
const [time, setTime] = useState<number>(0)
const [random, setRandom] = useState<number>(0)
return (
<div>
<button onClick={() => setTime(new Date().getTime())}>获取当前时间</button>
<button onClick={() => setRandom(Math.random())}>获取当前随机数</button>
<Show time={time}>{random}</Show>
</div>
)
}
type Data = {
time: number
}
// 子组件
const Show:React.FC<Data> = ({ time, children }) => {
function changeTime(time: number): string {
console.log('changeTime excuted...')
return new Date(time).toISOString()
}
return (
<div>
<p>Time is: { changeTime(time) }</p>
<p>Random is: { children }</p>
</div>
)
}
export default Example
在这个例子中,无论你点击的是 获取当前时间 按钮还是 获取当前随机数 按钮, <Show />
这个组件中的方法 changeTime
都会执行。
但事实上,点击 获取当前随机数 按钮改变的只会是 children
这个参数,但我们的 changeTime
也会因为子组件的重新渲染而重新执行,这个操作是很没必要的,消耗了无关的性能。
使用 useMemo
改造我们的 <Show />
子组件:
const Show:React.FC<Data> = ({ time, children }) => {
function changeTime(time: number): string {
console.log('changeTime excuted...')
return new Date(time).toISOString()
}
const newTime: string = useMemo(() => {
return changeTime(time)
}, [time])
return (
<div>
<p>Time is: { newTime }</p>
<p>Random is: { children }</p>
</div>
)
}
这个时候只有点击 获取当前时间 才会执行 changeTime
这个函数,而点击 获取当前随机数 已经不会触发该函数执行了。
2.你可能会好奇, useMemo
能做的难道不能用 useEffect
来做吗?
答案是否定的!如果你在子组件中加入以下代码:
const Show:React.FC<Data> = ({ time, children }) => {
//...
useEffect(() => {
console.log('effect function here...')
}, [time])
const newTime: string = useMemo(() => {
return changeTime(time)
}, [time])
//...
}
你会发现,控制台会打印如下信息:
> changeTime excuted...
> effect function here...
正如我们一开始说的:传入 useMemo
的函数会在渲染期间执行。在此不得不提 React.memo
,它的作用是实现整个组件的 Pure
功能:
const Show:React.FC<Data> = React.memo(({ time, children }) => {...}
所以简单用一句话来概括 useMemo
和 React.memo
的区别就是:前者在某些情况下不希望组件对所有 props
做浅比较,只想实现局部 Pure
功能,即只想对特定的 props
做比较,并决定是否局部更新。
useCallback
为啥使用useCallback?
useMemo
和 useCallback
接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo
返回的是函数运行的结果, useCallback
返回的是函数。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
怎么使用useCallback?
function changeName(name) {
return name + '给name做点操作返回新name'
}
const getNewName = useMemo(() => {
return changeName(name)
}, [name])
场景举例
将之前 useMemo
的例子,改一下子组件以下地方就OK了:
const Show:React.FC<Data> = ({ time, children }) => {
//...
const getNewTime = useCallback(() => {
return changeTime(time)
}, [time])
return (
<div>
<p>Time is: { getNewTime() }</p>
<p>Random is: { children }</p>
</div>
)
}
useReducer
为什么使用useReducer?
有没有想过你在某个组件里写了很多很多的 useState
是什么观感?比如以下:
const [name, setName] = useState<string>('')
const [islogin, setIsLogin] = useState<boolean>(false)
const [avatar, setAvatar] = useState<string>('')
const [age, setAge] = useState<number>(0)
//...
怎么使用useReducer?
import React, { useState, useReducer } from 'react'
type StateType = {
count: number
}
type ActionType = {
type: 'reset' | 'decrement' | 'increment'
}
const initialState = { count: 0 }
function reducer(state: StateType, action: ActionType) {
switch (action.type) {
case 'reset':
return initialState
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
return state
}
}
function Counter({ initialCount = 0}) {
const [state, dispatch] = useReducer(reducer, { count: initialCount })
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
)
}
export default Counter
场景举例:
与 useContext
结合代替 Redux 方案,往下阅读。
useContext
为啥使用useContext?
简单来说 Context
的作用就是对它所包含的组件树提供全局共享数据的一种技术。
怎么使用useContext?
export const ColorContext = React.createContext({ color: '#1890ff' })
const { color } = useContext(ColorContext)
// 或
export const ColorContext = React.createContext(null)
<ColorContext.Provider value='#1890ff'>
<App />
</ColorContext.Provider>
// App 或以下的所有子组件都可拿到 value
const color = useContext(ColorContext) // '#1890ff'
场景举例
1.根组件注册,所有子组件都可拿到注册的值:
import React, { useContext } from 'react'
const ColorContext = React.createContext<string>('')
const App = () => {
return (
<ColorContext.Provider value='#1890ff'>
<Father />
</ColorContext.Provider>
)
}
const Father = () => {
return (
<Child />
)
}
const Child = () => {
const color = useContext(ColorContext)
return (
<div style={{ backgroundColor: color }}>Background color is: { color }</div>
)
}
export default App
2.配合 useReducer
实现 Redux 的代替方案:
import React, { useReducer, useContext } from 'react'
const UPDATE_COLOR = 'UPDATE_COLOR'
type StateType = {
color: string
}
type ActionType = {
type: string,
color: string
}
type MixStateAndDispatch = {
state: StateType,
dispatch?: React.Dispatch<ActionType>
}
const reducer = (state: StateType, action: ActionType) => {
switch(action.type) {
case UPDATE_COLOR:
return { color: action.color }
default:
return state
}
}
const ColorContext = React.createContext<MixStateAndDispatch>({
state: { color: 'black' },
})
const Show = () => {
const { state, dispatch } = useContext(ColorContext)
return (
<div style={{ color: state.color }}>
当前字体颜色为: {state.color}
<button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'red'})}>红色</button>
<button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'green'})}>绿色</button>
</div>
)
}
const Example = ({ initialColor = '#000000' }) => {
const [state, dispatch] = useReducer(reducer, { color: initialColor })
return (
<ColorContext.Provider value={{state, dispatch}}>
<div>
<Show />
<button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'blue'})}>蓝色</button>
<button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'lightblue'})}>轻绿色</button>
</div>
</ColorContext.Provider>
)
}
export default Example
以上是关于hooks和typescript使用的主要内容,如果未能解决你的问题,请参考以下文章
如何在 React with Typescript 中使用和指定通用 Hooks?
使用 airbnb 和 prettier 扩展的 ESLint 配置,用于使用 typescript、jest 和 react-hooks 的 react 项目
如何为 React Action Hook Store 定义 Typescript 属性和类型
将 GraphQL 片段与 Apollo Hooks 一起使用时出错