Hooks

161 阅读14分钟

Hooks学习

一、高阶组件复习

1.高阶组件的缺点:

  • 难以溯源。如果原始组件A通过好几个HOC的构造,最终生成了组件B,不知道哪个属性来自于哪个HOC,需要翻看每个HOC才知道各自做了什么事情,使用了什么属性。
  • props属性名的冲突。某个属性可能被多个HOC重复使用。
  • 静态构建。新的组件是在页面构建之前生成,先有组件,后生成页面。

2.相对高阶组件的优点:

  • 不用担心props的命名冲突的问题
  • 可以溯源,子组件的props一定来自父组件。
  • 是动态构建的,页面在渲染后,可以动态地决定渲染哪个组件。
  • 所有能用HOC完成的事情,Render Props都可以做,且更加灵活。
  • 除了功能复用,还可以用作两个组件的单向数据传递。

3.渲染函数和高阶组件共同的缺点:

  • 需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。

4.

//App.jsx
import React, { Component, Fragment } from 'react'
//Fragment会占位,但不会编译成html,简写<></>,不用导入
import withLayout from './hoc/withLayout'
 class App extends Component {
  render() {
    return (
    <>
    <div>周末会见面</div>
      <div>周末会见面</div>
    </>
    )
  }
}
export default withLayout(App)
import React, { Component } from "react";
​
//高阶组件(本质就是一个函数):传入一个组件,返回一个增强后的组件
//此高阶组件:统一布局,顶部和底部,全局统一//1.定义一个函数,函数中必须有一个参数,此参数指定为一个组件,此参数首字母大写
//2.在此函数中返回一个增强后的组件(类组件|函数组件)const withLayout = Cmp => {
    return class Layout extends Component{
        render(){
            return(
                <div>
                    <h1>头部</h1>
                    <Cmp/>
                    <h2>底部</h2>
                </div>
            )
        }
    } 
}
//传入一个组件,返回一个增强后的组件
export default withLayout

高价组件使用props数据传递

//App.jsx
import React, { Component, Fragment } from 'react'
//Fragment会占位,但不会编译成html,简写<></>,不用导入
import withLayout from './hoc/withLayout'
​
class Child extends Component{
    render(){
        return(
            <div>
                儿子
            </div>
        )
    }
}
//调用高阶组件
const Childcmp = withLayout(Child)
 
 class App extends Component {
  render() {
    return (
    <>
    <div>周末会见面</div>
      <div>周末会见面</div>
      <Childcmp title="123" name = "哈哈"/>
    </>
    )
  }
}
export default withLayout(App)
import React, { Component } from "react";
​
//高阶组件(本质就是一个函数):传入一个组件,返回一个增强后的组件
//此高阶组件:统一布局,顶部和底部,全局统一//1.定义一个函数,函数中必须有一个参数,此参数指定为一个组件,此参数首字母大写
//2.在此函数中返回一个增强后的组件(类组件|函数组件)
​
const withLayout = Cmp => {
    return class Layout extends Component{
        render(){
            return(
                <div>
                    <h1>头部</h1>
                    {/* 把父组件传过来的props,代理传给子组件 */}
                    <Cmp {...this.props} />
                    <h2>底部</h2>
                </div>
            )
        }
    } 
}
export default withLayout

props不能直接修改,但是可以深复制,在复制后的对象进行修改

引入lodash

import _ from 'lodash'
let proxyData = _.cloneDeep(this.props)
proxyData.age = proxyData.age > 12 '保密' : proxyData.age

二、class 组件的问题

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。如果不使用 ES2022 public class fields,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景

import { Component } from 'react'
​
class Counter extends Component {
  state = {
    counter: 0
  }
​
  handleClick = () => {
    this.setState((state) => ({
      counter: state.counter + 1
    }))
  }
​
  render () {
    return (
      <>
        <div>{this.state.counter}</div>
        <button onClick={this.handleClick}>add</button>
      </>
    )
  }
}
​
export default Counter

三、Hooks的引入

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。

// 03-counter-sample/HooksCounter.jsximport React, { useState } from 'react'export default function HooksCounter() {
  const [counter, setCounter] = useState(0)
​
  const handleClick = () => {
    setCounter(counter + 1)
  }
​
  return (
    <>
      <div>{counter}</div>
      <button onClick={handleClick}>add</button>
    </>
  )
}

现在,hooks 看上去也没有什么不同,只是少了几行代码而已。我们继续给类组件添加功能:

// 03-counter-sample/ClassCounter.jsximport { Component } from 'react'class Counter extends Component {
  state = {
    counter: 0
  }
​
  handleClick = () => {
    this.setState((state) => ({
      counter: state.counter + 1
    }))
  }
​
  componentDidMount () {
    document.title = this.state.counter
  }
​
  componentDidUpdate () {
    document.title = this.state.counter
  }
​
  render () {
    return (
      <>
        <div>{this.state.counter}</div>
        <button onClick={this.handleClick}>add</button>
      </>
    )
  }
}
​
export default Counter

再看看 Hooks 组件如何实现:

// 03-counter-sample/HooksCounter.jsx

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

export default function HooksCounter() {
  const [counter, setCounter] = useState(0)

  useEffect(() => {
    document.title = counter
  })

  const handleClick = () => {
    setCounter(counter + 1)
  }

  return (
    <>
      <div>{counter}</div>
      <button onClick={handleClick}>add</button>
    </>
  )
}

在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。再看一个例子。

// hooks/04-custom-hooks/useCounter.js

import { useState } from 'react'

const useCounter = (defaultCounter) => {
  const [counter, setCounter] = useState(defaultCounter)

  const handleAddClick = () => {
    setCounter(counter + 1)
  }

  const handleDoubleClick = () => {
    setCounter(counter * 2)
  }

  return {
    counter,
    handleAddClick,
    handleDoubleClick
  }
}

export default useCounter
// hooks/04-custom-hooks/useTitle.js

import { useEffect } from 'react'

const useTitle = (counter) => {
  useEffect(() => {
    document.title = counter
  })
}

export default useTitle
// hooks/04-custom-hooks/Counter.js

import React from 'react'
import useCounter from './useCounter'
import useTitle from './useTitle'
import DoubleCounter from './DoubleCounter'

export default function HooksCounter() {
  const { counter, handleAddClick } = useCounter(0)
  useTitle(counter)

  return (
    <>
      <div>{counter}</div>
      <button onClick={handleAddClick}>add</button>

      <DoubleCounter></DoubleCounter>
    </>
  )
}
// hooks/04-custom-hooks/DoubleCounter.js

import React from 'react'
import useCounter from './useCounter'
import useTitle from './useTitle'

export default function DoubleCounter() {
  const { counter, handleDoubleClick } = useCounter(1)
  useTitle(counter)

  return (
    <>
      <div>{ counter }</div>
      <button onClick={ handleDoubleClick }>double</button>
    </>
  )
}

四、Hooks详情

1.useState

  1. 使用多个state变量

    // 声明多个 state 变量
    const [age, setAge] = useState(42);
    const [fruit, setFruit] = useState('banana');
    const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
    

    如果你之前用过 class,你或许会试图总是在一次 useState() 调用中传入一个包含了所有 state 的对象。如果你愿意的话你可以这么做。这里有一个跟踪鼠标移动的组件的例子。我们在本地 state 中记录它的位置和尺寸:

    const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
    

    整合成一个例子:

    // hooks/05-useState/MultipleState.jsx
    
    import React, { useState, useEffect } from 'react'
    import usePosition from './usePosition'
    
    export default function MultipleState() {
      const [age, setAge] = useState(42)
      const [fruit, setFruit] = useState('banana')
      const [todos, setTodos] = useState([{ text: '学习 Hook' }])
      const { position } = usePosition()
    
      // const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
    
      // const [position, setPosition] = useState({ left: 0, top: 0 });
      const [size, setSize] = useState({ width: 100, height: 100 });
    
      // useEffect (() => {
      //   function handleWindowMouseMove(e) {
      //     // 展开 「...state」 以确保我们没有 「丢失」 width 和 height
      //     // setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
      //     setPosition( position => ({ left: e.pageX, top: e.pageY }))
      //   }
      //   // 注意:这是个简化版的实现
      //   window.addEventListener('mousemove', handleWindowMouseMove)
      //   return () => window.removeEventListener('mousemove', handleWindowMouseMove)
      // }, [])
      
      return (
        <>
          <h1>{ age } <button onClick={ () => setAge(age + 1) }>change</button></h1>
          <h1>{ fruit }</h1>
          <h1>{ JSON.stringify(todos) }</h1>
          {/* <h1>{ state.left } / { state.top }</h1>
          <h1>{ state.width } / { state.height }</h1> */}
          <h1>{ position.left } / { position.top }</h1>
          <h1>{ size.width } / { size.height }</h1>
        </>
      )
    }
    
    // hooks/05-useState/usePosition.js
    
    import { useState, useEffect } from 'react'
    
    const usePosition = () => {
      const [position, setPosition] = useState({ left: 0, top: 0 });
      useEffect (() => {
        function handleWindowMouseMove(e) {
          setPosition( position => ({ left: e.pageX, top: e.pageY }))
        }
        // 注意:这是个简化版的实现
        window.addEventListener('mousemove', handleWindowMouseMove)
        return () => window.removeEventListener('mousemove', handleWindowMouseMove)
      }, [])
    
      return {
        position
      }
    }
    
    export default usePosition
    
  2. 多状态声明的注意事项

    // hooks/05-useState/HooksRules.js
    
    import React, { useState, useEffect } from 'react'
    import usePosition from './usePosition'
    
    export default function MultipleState() {
      let average = 30
      
      // React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
      if (average >= 30) {
        const [age, setAge] = useState(42)
        average = 35
      }
      
      const [fruit, setFruit] = useState('banana')
      const [todos, setTodos] = useState([{ text: '学习 Hook' }])
      
      return (
        <>
          <h1>{ age } <button onClick={ () => setAge(age + 1) }>change</button></h1>
          <h1>{ fruit }</h1>
          <h1>{ JSON.stringify(todos) }</h1>
        </>
      )
    }
    
  3. 注意:

    • 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

    • 只在 React 函数中调用 Hook, 不要在普通的 JavaScript 函数中调用 Hook。你可以:

      • 在 React 的函数组件中调用 Hook
      • 在自定义 Hook 中调用其他 Hook

2.useEffect

  1. useEffect()是个副作用函数
  2. useEffect()函数每次组件渲染时,可再次被调用
  3. 在开发环境中,开启了 React.StrictMode 模式,组件开始时被渲染两次。
  4. useEffect()通过传递第二个参数数组来提高渲染性能,或者说实现watch效果
import React, { useState, useEffect } from 'react'

export default function Counter() {
  const [ counter, setCounter ] = useState(0)
  const [ title, setTitle ] = useState('hello')

  useEffect (() => {
    const timer = setTimeout(() => {
      console.log(counter)
    }, 1000)

    return () => {
      clearInterval(timer)
      console.log('组件被卸载了!')
    }
  })

  const handleAddClick = () => {
    setCounter(counter + 1)
  }

  const handleChangeTitle = () => {
    setTitle('world')
  }

  useEffect (() => {
    setCounter(50)
  }, [])

  useEffect (() => {
    console.log('title变化成:' + title)
  }, [title, counter])
  
  return (
    <>
      <div>{ counter }</div>
      <button onClick={handleAddClick}>add</button>
      <div>{ title }</div>
      <button onClick={handleChangeTitle}>change</button>
    </>
  )
}

3.useRef

  1. 利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离

// hooks/07-useRef/Temp.jsx

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

  // useEffect(() => {
  //   document.title = `You clicked ${count} times`;
  // });

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

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

export default Temp
  1. 可以利用 useRef 实现 componentDidUpdate

    // hooks/06-useEffect/useUpdate.js
    
    import { useEffect, useRef } from 'react'
    
    const useUpdate = (fn) => {
      const mounted = useRef(false)
      useEffect(() => {
        if (mounted.current) {
          fn()
        } else {
          mounted.current = true
        }
      })
    }
    
    export default useUpdate
    

3.useReducer

useReducer 践行了 Flux/Redux 思想。使用步骤:

1、创建初始值initialState

2、创建所有操作 reducer(state, action);

3、传给 userReducer,得到读和写API

4、调用写 ({type: '操作类型'})

总的来说,useReducer 是 useState 的复杂版。

// hooks/08-useReducer/ReducerDemo.jsx

import { useReducer } from 'react'

const defaultValue = {
  count: 0
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'add':
      return {
        count: state.count + 1
      }
    case 'minus':
      return {
        count: state.count - 1
      }

    default:
      throw new Error('unknown type')
  }
}

const ReducerDemo = () => {
  const [state, dispatch] = useReducer(reducer, defaultValue)

  return (
    <>
      <div>{ state.count }</div>
      <button onClick={() => dispatch({ type: 'add' })}>add</button>
    </>
  )
}

export default ReducerDemo

4.useContext

1、使用 C = createContext(initial) 创建上下文

2、使用 <C.Provider> 圈定作用域

3、在作用域内使用 useContext(C)来使用上下文

// hooks/09-useContext/contextCreator.js

import { createContext } from 'react'

const context = createContext(null)

export default context
// hooks/09-useContext/ContextDemo.js

import React, { useState } from 'react'
import Child1 from './Child1'
import Child2 from './Child2'
import context from './contextCreator'

export default function ContextDemo() {
  const [count, setCount] = useState(0)

  return (
    <context.Provider value={{count, setCount}}>
      <h1>Parent</h1>
      <Child1></Child1>
      <Child2></Child2>
    </context.Provider>
  )
}
// hooks/09-useContext/Child1.jsx

import React, { useContext } from 'react'
import context from './contextCreator'

export default function Child1() {
  const { count } = useContext(context)

  return (
    <h1>Child1: { count }</h1>
  )
}
// hooks/09-useContext/Child1.jsx

import React, { useContext } from 'react'
import context from './contextCreator'

export default function Child1() {
  const { count } = useContext(context)

  return (
    <h1>Child1: { count }</h1>
  )
}

5.useReducer和useContext

  1. 将数据集中在一个store对象
  2. 将所有操作集中在reducer
  3. 创建Context
  4. 创建读取数据得API
  5. 将API放入Context
  6. 用Context.Provider将Context提供给所有组件
  7. 各个组件用useContext获取读写API
// books/10-liteRedux/data.js

const ajax = (path) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      switch (path) {
        case '/user':
          resolve({
            id: 1,
            name: 'Felixlu'
          })
          break
        case '/books':
          resolve([
            {
              id: 1,
              name: 'JavaScript程序设计'
            },
            {
              id: 2,
              name: 'React深入浅出'
            }
          ])
          break
        case '/movies':
          resolve([
            {
              id: 1,
              name: '人生大事'
            },
            {
              id: 2,
              name: '神探大战'
            }
          ])
          break
        default:
  
      }
    }, 1000)
  })
}

export default ajax
// books/10-liteRedux/store.js

const store = {
  user: null,
  books: null,
  movies: null
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'setUser':
      return {
        ...state,
        user: action.user
      }
    case 'setBooks':
      return {
        ...state,
        books: action.books
      }
    case 'setMovies':
      return {
        ...state,
        movies: action.movies
      }
    default:
      throw new Error('error')
  }
}

export {
  store,
  reducer
}
// books/10-liteRedux/contextCreator.js

import { createContext } from 'react'

const context = createContext(null)

export default context
// books/10-liteRedux/LiteReduxDemo.js

import React, { useReducer } from 'react'
import { store, reducer } from './store'
import context from './contextCreator'

import User from './User'
import Books from './Books'
import Movies from './Movies'

export default function LiteReduxDemo() {
  const [state, dispatch] = useReducer(reducer, store)

  const api = {
    state,
    dispatch
  }

  return (
    <context.Provider value={api}>
      <User></User>
      <Books></Books>
      <Movies></Movies>
    </context.Provider>
  )
}
// books/10-liteRedux/User.js

import React, { useEffect, useContext } from 'react'
import ajax from './data'
import context from './contextCreator'

export default function User() {
  const { state, dispatch } = useContext(context)

  useEffect(() => {
    ajax('/user').then((user) => {
      dispatch({
        type: 'setUser',
        user
      })
    })
  }, [dispatch])

  return (
    <div>User:{state.user && state.user.name}</div>
  )
}
// books/10-liteRedux/Books.js

import React, { useContext, useEffect } from 'react'
import context from './contextCreator'
import ajax from './data'

export default function Books() {
  const { state, dispatch } = useContext(context)

  useEffect(() => {
    ajax('/books').then((books) => {
      dispatch({
        type: 'setBooks',
        books
      })
    })
  }, [dispatch])

  return (
    <div>Books:
      {
        state.books && state.books.map((book) => {
          return (
            <span key={book.id}>{book.name}、</span>
          )
        })
      }
    </div>
  )
}
// books/10-liteRedux/Movies.js

import React, { useContext, useEffect } from 'react'
import ajax from './data'
import context from './contextCreator'

export default function Movies() {
  const {state, dispatch} = useContext(context)

  useEffect(() => {
    (async () => {
      const movies = await ajax('/movies')
      dispatch({
        type: 'setMovies',
        movies
      })
    })()
  })

  return (
    <div>Movies:
      {
        state.movies && state.movies.map((movie) => {
          return <span key={movie.id}>{movie.name}、</span>
        })
      }
    </div>
  )
}

上面这个例子中与直接转发 ref 不同,直接转发 ref 是将 React.forwardRef 中函数上的 ref 参数直接应用在了返回元素的 ref 属性上,其实父、子组件引用的是同一个 ref 的 current 对象,官方不建议使用这样的 ref 透传,而使用 useImperativeHandle 后,可以让父、子组件分别有自己的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 方法来自定义开放给父组件的 current。

useImperativeHandle 的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样可以像上面的案例一样,通过自定义父组件的 ref 来使用子组件 ref 的某些方法。

useImperativeHandle 和 React.forwardRef 必须是配合使用的,这也是为什么在开头要介绍 ref 的转发。

6.useLayoutEffect

  1. useLayoutEffectuseEffect的区别

    • useEffect 是异步执行的,而useLayoutEffect是同步执行的。
    • useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前
import React, { useState, useLayoutEffect, useEffect } from 'react'

export default function UseLayoutEffectDemo() {
  const [ state, setState ] = useState('hello')

  useLayoutEffect(() => {
    let i = 0
    while (i < 1000000000) {
      i++
    }
    setState('world')
  }, [])

  return (
    <>
      <h1>{state}</h1>
    </>
  )
}

7.useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

// hooks/14-useDebugValue/UseDebugValueDemo.jsx

import React from 'react'
import useMyHook from './useMyHook'

export default function UseDebugValueDemo() {
  const {x} = useMyHook()
  return (
    <h1>{ x }</h1>
  )
}
// hooks/14-useDebugValue/useMyHook.js

import { useDebugValue, useState } from 'react'

const useMyHook = () => {
  const [x, setX] = useState(0)
  
  useDebugValue('tag' + x)
  
  // useDebugValue(num, num => num + 1 )

  return {
    x: 100
  }
}

export default useMyHook

8.useId

useId是一个钩子,用于生成唯一的ID,在服务器和客户端之间是稳定的,同时避免水化不匹配。

注意:

useId不是用来生成列表中的键的。Keys 应该从你的数据中生成。

对于一个基本的例子,直接将id传递给需要它的元素。

function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react"/>
    </>
  );
};

对于同一组件中的多个ID,使用相同的ID附加一个后缀。

function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + '-firstName'}>First Name</label>
      <div>
        <input id={id + '-firstName'} type="text" />
      </div>
      <label htmlFor={id + '-lastName'}>Last Name</label>
      <div>
        <input id={id + '-lastName'} type="text" />
      </div>
    </div>
  );
}

注意:

useId 会生成一个包含 : token的字符串。这有助于确保令牌是唯一的,但在CSS选择器或API(如querySelectorAll)中不支持。

useId支持一个identifierPrefix,以防止在多根应用程序中发生碰撞。要配置,请参阅 hydrateRootReactDOMServer的选项。

9. useDeferredValue

useDeferredValue 需要接收一个值, 返回这个值的副本, 副本的更新会在值更新渲染之后进行更新, 以此来避免一些不必要的重复渲染. 打个比方页面中有输入框, 输入框下的内容依赖于输入框的值, 但是输入框是一个高频操作, 如果输入10次, 可能用户只想看到最终的结果那么中途的实时渲染就显得不那么重要了, 页面元素少点还好, 一旦元素过多页面就会及其的卡顿, 渲染引擎堵得死死的, 用户就会骂娘了, 此时使用useDeferredValue是一个很好的选择。

// hooks/16-UseDeferredValue/UseDeferredValueDemo.jsx

import React, { useDeferredValue, useState, useMemo } from 'react'
import List from './List'

export default function UseDeferredValueDemo() {
  const [inpVal, setInpVal] = useState('')
  const deferredValue = useDeferredValue(inpVal)
  const memoList = useMemo(() => <List count={deferredValue}></List>, [deferredValue])
  return (
    <>
      <h1>UseDeferredValue</h1>
      <input type="number" value={inpVal} max={200000} onChange={(e) => setInpVal(e.target.value)}/>
      {memoList}
    </>
  )
}
// hooks/16-UseDeferredValue/List.jsx

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

export default memo(function List({count}) {
  const [data, setData] = useState([])

  useEffect(() => {
    const data = []
    data.length = +5000
    for (let i = 0; i < data.length; i++) {
      data.fill(i+1, i)
    }
    setData(data)

  }, [count])

  return (
    <div>
      {
        data.map((item) => {
          return (
            <p key={item}>{count}</p>
          )
        })
      }
    </div>
  )
})

10. useTransition

useTransition 又叫过渡, 他的作用就是标记非紧急更新, 这些被标记非紧急更新会在紧急更新完之后进行更新, useTransition 使用场景在应对渲染量很大的页面,需要及时响应某些事件的情况。

举个例子,准备一个进度条, 通过滑动进度条来显示进度条的进度并且渲染相同进度数量的div, 如果我们不对渲染进行优化那无疑页面会很卡, 此时使用过渡配合useMemo来缓存页面结构, diffing算法就会对比出少量的变化进行局部修改。

import React, { useTransition, useState, useMemo } from 'react'
 
export default function UseTransition() {
  const [isPending, startTransition] = useTransition()
 
  const [rangeValue, setRangeValue] = useState(1)
  const [renderData, setRenderData] = useState([1])
  const [isStartTransition, setIsStartTransition] = useState(false)
  const handleChange = (e) => {
    setRangeValue(e.target.value)
    const arr = []
    arr.length = e.target.value
    for (let i = 0; i <= arr.length; i++) {
      arr.fill(i, i + 1)
    }
    if (isStartTransition) {
      startTransition(() => {
        setRenderData(arr)
      })
    } else {
      setRenderData(arr)
    }
  }
  const jsx = useMemo(() => {
    return renderData.map((item, index) => {
      return (
        <div
          style={{
            width: 50,
            height: 50,
            backgroundColor: `#${Math.floor(Math.random() * 16777215).toString(
              16
            )}`,
            margin: 10,
            display: 'inline-block',
          }}
          key={'item'+index}
        >
          {item}
        </div>
      )
    })
  }, [renderData])
  return (
    <div>
      <div style={{ textAlign: 'center' }}>
        <label>
          <input
            type="checkbox"
            checked={isStartTransition}
            onChange={(e) => {
              setIsStartTransition(e.target.checked)
            }}
          />
          useTransition
        </label>
        <input
          type="range"
          value={rangeValue}
          min={0}
          max={10000}
          style={{ width: 120 }}
          onChange={handleChange}
        />
        <span>进度条 {rangeValue}</span>
        <hr />
      </div>
      {jsx}
    </div>
  )
}

13.useSyncExternalStore

const state = useSyncExternalStore(subscribe, getSnapshot);

React18的beta版本将useMutableSource更新为了useSyncExternalStore,这个新的api将会对React的各种状态管理库产生非常大的影响,下面我来介绍useSyncExternalStore的用法和场景。

我们可以通过这个api自行设计一个redux + react-redux的数据方案:

1、设计store

首先我们要设计一个store,它必须有如下属性:

  • currentState:当前状态
  • subscribe:提供状态发生变化时的订阅能力
  • getSnapshot: 获取当前状态

以及改变state的方法,这里参考redux,设计了dispatch、reducer

// hooks/18-useSyncExternalStore/store.js

const store = {
    currentState:{data:0},
    listeners:[],
    reducer(action){
        switch(action.type) {
            case 'ADD':
                return {data:store.currentState.data+1}
            default:
                return store.state
        }
    },
    subscribe(l){
        store.listeners.push(l)
    },
    getSnapshot() {
        return store.currentState
    },
    dispatch(action) {
        store.currentState = store.reducer(action)
        store.listeners.forEach(l=>l())
        return action;
    }
}

2、应用 store 同步组件状态

// hooks/18-useSyncExternalStore/UseSyncExternalStoreDemo.jsx

import React, { useSyncExternalStore } from 'react'
import store from './store'

export default function UseSyncExternalStoreDemo() {
    //store.subscribe将组件注入到监听器里
  const state = useSyncExternalStore(store.subscribe, () => store.getSnapshot().data);
    
    return (
      <div>
        <div>count: {state}</div>
        <div>
            <button onClick={()=>store.dispatch({type:'ADD'})}>add+</button>
        </div>
      </div>
    )
}

14.useInsertionEffect

useInsertionEffect(didUpdate);

useInsertionEffect 与useEffect相同,在所有DOM变更之前同步触发。在使用 useLayoutEffect 读取布局之前,使用这个函数将样式注入到DOM中。因为这个钩子的作用域是有限的,所以这个钩子不能访问 refs,也不能调度更新。

import React, { useInsertionEffect, useEffect, useLayoutEffect } from 'react'
 
export default function UseInsertionEffect() {

  useInsertionEffect(() => {
    console.log('useInsertionEffect')
    // const style = document.createElement('style')
    // style.innerHTML = '.box { color: red }'
    // document.head.appendChild(style)
  })

  useEffect(() => {
    console.log('useEffect')
  })

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
  })

  return (
    <div className="box">UseInsertionEffect</div>
  )
}