react笔记

useState

会有人直接 state.value = hoge, 所以如果 state 在 set 之后不知道为啥和 set 的值变得不一样的话, 八成就是因为这个了


// prevState
const [state, setState] = useState([
{ key: 'a', value: [111] },
{ key: 'b', value: [222] },
])
// filteredData
const filteredData = state.filter((data) => data.key !== 'b') // [{key:'a', value: [111]}]
// addData
const addData = [{ key: 'b', value: [333] }]
// mergedData
const mergedData = [...filteredData, ...addData] // [{key:'a', value: [111]}, {key:'b', value: [333]}]
setState(mergedData)
// 结果最后setState完了之后state里面的key: 'b'的value还是之前的[222]
console.log(tate) // [{key:'a', value: [111]}, {key:'b', value: [222]}]
// 原因就是因为在setState执行之后, 其他地方直接修改了state, 导致渲染后的state还是之前的里面的key: 'b'的value还是之前的[222]
const targetState = state.find((item) => item.key === 'b')
targetState.value = [222]

通过执行回调函数更改 state 的时候, 有一种特殊情况会导致回调函数虽然执行了但是 state 没有更改成功


function Component1({ loading }) {
// loading的值从false变成true再变回false变化的时候会使Form被卸载之后再渲染
// 导致Form组件内部的state被重置
// 如果在Form里面义了更改Form的state用的回调函数 并且在别的组件使用了这个回调函数的话
// 回调函数会被执行 但是state不会被更改(因为要更改的state所在的组件已经被卸载掉了)
return <>{loading ? <></> : <Form />}</>
}

遇到这种情况的话, 可以考虑以下写法


function Component2({ loading }) {
// 如果想让组件在loading为true的时候也不被卸载的话可以用style
return <Form style={{ display: loading ? 'none' : 'flex' }} />
}

useRef

ref.current 就算放到 useEffect 的deps里或者作为组件的props,也不会在变化的时候触发组件重新渲染或者 useEffect 的第一个参数的再执行


const Text = ({val})=> {
// 点击按钮之后这条log也不会被打印
console.log(val)
return (
<div>{val}</div>
)
}
const TestRef = () => {
const ref = useRef('test')
useEffect(()=> {
// 点击按钮之后这条log也不会被打印
console.log(ref.current)
}, [ref.current])
return (
<>
<button onClick={()=> ref.current = 'new val'}>
click me
</button>
<Text val={ref.current} />
</>
)
}

定义 ref 的时候初始值设置为null的话会被认为是只读 ref


const elementRef = useRef<string>(null)
elementRef.current = 'hoge' // 报错ts2540

原理是用了函数重载,当传入的泛型参数 T 不包含 null 时返回RefObject<T>,当包含 null 时将返回 MutableRefObject<T>, 而RefObject<T>的 current 为只读


function useRef<T>(initialValue: T): MutableRefObject<T>
function useRef<T>(initialValue: T | null): RefObject<T>
interface RefObject<T> {
readonly current: T | null
}
interface MutableRefObject<T> {
current: T
}

所以如果想让 ref 是可修改的话, 需要加上| null


const elementRef = useRef<string | null>(null)
elementRef.current = 'hoge' // 不报错

useMemo

  1. 缓存的是值
  2. 第一个参数是需要缓存的值或者是计算后再返回值的函数

useCallback

  1. 缓存的是函数
  2. 第一个参数是需要缓存的函数
  3. useCallback不是为了节省创建函数的开销,而是为了不使函数发生变化以导致渲染开销大的组件重新渲染
  4. 就算用useCallback,子组件不用React.memo的话就还是无法避免重新渲染
  5. useStatesetState被 callback 化了,所以加不加到依赖数组里面都不会影响渲染

性能优化

  1. 本质: 将变的部分不变的部分分离
    1. props
    2. const A = () => {} 这样的没有 props, state, context 的组件 其实是有 props 的 这个 props 是 {} 所以在对比 props 的时候会 {} !== {} 导致 rerender
    3. 这种没有 props 的组件的父组件渲染的时候 会重新创建一个 props 所以会导致 {} !== {} 导致 rerender
    4. state
    5. context
  2. 当父组件满足性能优化条件 子孙组件可能命中性能优化
  3. 返回值不变的时候会自动优化

React.memo()

  1. 用了这个之后 对比 props 的时候会用浅比较(比较对象中的元素)而不是全等比较

Custom Hook

  • 返回数组的时候用as const可以使返回类型不会成为联合类型的数组而是元组, 这样的话在结构的时候 ts 可以根据解构位置推断出来正确的类型
  • 不过 React 团队建议返回值为 2 个以上的 Custom Hook 应该使用对象{}而不是数组[]

import { useState } from 'react'
export function useLoading() {
const [isLoading, setState] = useState(false)
const load = (aPromise: Promise<any>) => {
setState(true)
return aPromise.finally(() => setState(false))
}
return [isLoading, load] as const // 推断为类型为[boolean, typeof load]的元组而不是为联合类型(boolean | typeof load)的数组(boolean | typeof load)[]
}

props type

React.ComponentProps

可以使用内置的泛型工具直接获取 props 的类型, 这样获取到的 props 的类型是真实的类型声明, 而使用 library 导出的类型声明有时候是不完整的(比如有的会缺少children)


import React from 'react'
import { AnyComponent } from 'any-library'
type AnyComponentProps = React.ComponentProps<typeof AnyComponent>

源码


type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
T extends JSXElementConstructor<infer P>
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: {}

React.ComponentRef

同样也有可以获取 ref 类型的泛型工具


import React from 'react'
import { AnyRefComponent } from 'any-library'
type AnyComponentRef = React.ComponentRef<typeof AnyRefComponent>

源码


type ComponentRef<T extends ElementType> = T extends NamedExoticComponent<
ComponentPropsWithoutRef<T> & RefAttributes<infer Method>
>
? Method
: ComponentPropsWithRef<T> extends RefAttributes<infer Method>
? Method
: never

forwardRef

想在父组件使用子组件中定义的函数或者取得子组件中定义的值的时候,可以在子组件里定义第二个参数(ref)useImperativeHandle将内部的函数或者值传递给外部

然后使用forwardRef包住目标子组件

ChildComponent.tsx
Copy

// 这里定义想传递给外部的函数或者值的type
export interface ChildComponentHandlers {
handleSomeEvent: () => void
someData: number
}
const ChildComponent: ForwardRefRenderFunction<ChildComponentHandlers> = (_, ref) => {
const [someData, setSomeData] = useState(0)
const handleSomeEvent = () => {
// 逻辑
}
// 第二个参数的返回值是一个对象,想传递给外部的函数或者值要放在这个对象里面
useImperativeHandle(ref, () => ({
handleSomeEvent,
someData,
}))
return (
// jsx
)
}
export default forwardRef(ChildComponent)

这里将组件的type定义为ForwardRefRenderFunction<ChildComponentHandlers>之后再在export default的时候包住子组件,是因为如果在定义子组件的地方直接用 forwardRef 包住的话,eslint会提示component definition is missing display name forwardref

修改完子组件之后,就可以在父组件将useRef定义的ref传递给子组件,然后就可以用用这个ref来调用子组件内定义的函数或者获取子组件内部定义的值了

ParentComponent.tsx
Copy

const ParentComponent = () => {
const childComponentRef = useRef<ChildComponentHandlers>(null)
useEffect(()=> {
// 像这样获取子组件内部定义的值
const childSomeData = childComponentRef.current!.someData
// 像这样调用子组件内部的方法
childComponentRef.current.handleSomeEvent()
}, [])
return (
<ChildComponent ref={childComponentRef} />
)
}

QA

  1. Q: function 组件和箭头函数组件

    A: 箭头函数可以很简单地写 React.memo() 也可以很简单地用 React.FC 或者 NextPage 来约束类型

click-to-react-component

可以给调试模式增加一个按住 alt(option)键选中组件跳转至代码的功能,并且开箱即用


import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
// import click-to-react-component
import { ClickToComponent } from 'click-to-react-component'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
// 这里加上这行代码就搞定了!
<ClickToComponent />
<App />
</React.StrictMode>,
)

copy-to-clipboard

复制文本到剪贴板组件


// react-copy-to-clipboard
<CopyToClipboard onCopy={this.onCopy} text={this.state.value}>
<button onClick={this.onClick}>复制</button>
</CopyToClipboard>