React基础知识总结(五)

92 阅读18分钟

十七、hooks

17.1 为什么使用hooks

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props高阶组件

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

react 18版本以前hooks

17.2常见的hooks

17.2.1 useState

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'import App from './12_hooks/01App_useState'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    <React.StrictMode>
      <App root={ root }/>
    </React.StrictMode>
  </ErrorBoundary>
)
​

src/12_hooks/01App_useState.jsx

// src/12_hooks/01App_useState.jsximport React, { useState } from 'react';
​
const App = () => {
  // 设置了一个 变量 count,初始值为10,修改状态的方法叫 setCount
  const [count, setCount] = useState(10)
  /**
   * const [num, setNum] = useState(1)
   * const [arr, setArr] = useState([])
   * const [obj, setObj] = useState({})
   * const [flag, setFlag] = useState(false)
   */
​
  return (
    <div>
      { count }
      <button onClick={ () => {
        // setCount(count + 1)
        // setCount(count + 1)
        setCount((prevCount) => prevCount + 1)
        // setCount((prevCount) => prevCount + 1)
      }}>加1</button>
    </div>
  );
};
​
export default App;

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
import App from './12_hooks/02App_useState'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    <React.StrictMode>
      <App root={ root }/>
    </React.StrictMode>
  </ErrorBoundary>
)
​

src/12_hooks/02App_useState.jsx

// src/12_hooks/02App_useState.jsximport React, { useState } from 'react';
​
const App = () => {
​
  const [obj, setObj] = useState({
    w: 100,
    h: 100,
    x: 0,
    y: 0
  })
​
  const move = (event) => {
    // setObj({ // ❌
    //   x: event.clientX,
    //   y: event.clientY
    // })
​
    // setObj({ ...obj, x: event.clientX, y: event.clientY })
    setObj(Object.assign({}, obj, { x: event.clientX, y: event.clientY }))
  }
​
  return (
    <div style={ {width:'100vw', height: '100vh'} } onMouseMove={ move }>
      <div style={{ width: obj.w, height: obj.h, backgroundColor: '#f66'}}>
        宽为: { obj.w }, 高为: { obj.h }
      </div>
      <div>x: { obj.x }, y: { obj.y }</div>
    </div>
  );
};
​
export default App;

思考:如上效果虽然已经实现,但是设计合理吗

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
import App from './12_hooks/03App_useState'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    <React.StrictMode>
      <App root={ root }/>
    </React.StrictMode>
  </ErrorBoundary>
)
​

src/12_hooks/03App_useState.jsx

// src/12_hooks/03App_useState.jsx
​
import React, { useState } from 'react';
​
const App = () => {
​
  // const box = { w: 100, h: 100 }
  const [box] = useState({ w: 100, h: 100 })
  const [poi, setPoi] = useState({ x: 0, y: 0 })
​
​
  const move = (event) => {
    setPoi({ 
      x: event.clientX,
      y: event.clientY
    })
  }
​
  return (
    <div style={ {width:'100vw', height: '100vh'} } onMouseMove={ move }>
      <div style={{ width: box.w, height: box.h, backgroundColor: '#0f0'}}>
        宽为: { box.w }, 高为: { box.h }
      </div>
      <div>x: { poi.x }, y: { poi.y }</div>
    </div>
  );
};
​
export default App;
  • 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

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

    • 在 React 的函数组件中调用 Hook
    • 在自定义 Hook 中调用其他 Hook
  • 如果遇到需要以对象的形式定义状态时,根据需求划分对象,因为修改状态使用的是替换

17.2.2 useEffect

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
import App from './12_hooks/04App_useEffect'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/04App_useEffect.jsx

// src/12_hooks/04App_useEffect.jsximport React, { useState, useEffect } from 'react';
​
const App = (props) => {
  const [list, setList] = useState([])
  const [count, setCount] = useState(10)
​
  // useEffect(() => {}) // componentDidMount  componentDidUpdate
  // useEffect(() => {}, []) // componentDidMount  只执行一次
  // useEffect(() => {}, [ count ]) // 监听到count的值发生改变时,执行 - 监听器
  // useEffect(() => { return () => {} }, []) // 返回的函数相当于 componentWillUnmount
  useEffect(() => {
    fetch('http://121.89.205.189:3000/api/pro/list')
      .then(res => res.json())
      .then(res => {
        console.log(1, res.data)
        setList(res.data)
      })
    return () => {
      console.log('销毁1')
    }
  }, [])
  useEffect(() => {
    fetch('http://121.89.205.189:3000/api/pro/list')
      .then(res => res.json())
      .then(res => {
        console.log(2, res.data)
        setList(res.data)
      })
    return () => {
      console.log('销毁2')
    }
  }, [count])
  return (
    <div>
      <button onClick={ () => { setCount(count + 1) }}>加1</button>
      <button onClick={ () => { props.root.unmount() }}>销毁</button>
      <ul>
        {
          list && list.map(item => {
            return (
              <li key = { item.proid }> { item.proname } </li>
            )
          })
        }
      </ul>
    </div>
  );
};
​
export default App;
  1. useEffect() 是个副作用函数。
  2. useEffect() 函数在每次组件重新渲染时,可再次被调用。
  3. 在开发环境中,开启了 React.StrictMode 模式,组件开始时被渲染两次。zh-hans.legacy.reactjs.org/docs/strict…
  4. useEffect() 通过返回函数来清理副作用。
  5. useEffect() 通过传递第二个参数数组来提高渲染性能,或者说实现 watch 效果。

17.2.3 useRef

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
import App from './12_hooks/05App_useRef'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/05App_useRef.jsx

// src/12_hooks/05App_useRef.jsximport React, { Component, useRef, useEffect } from 'react';
​
const Fun = React.forwardRef((props, ref) => {
  return (
    <div>
      function 组件
      <input ref = { ref }/>
    </div>
  )
})
​
class Com extends Component {
  state = {
    com: 'class com'
  }
  render () {
    return (
      <div>
        class 组件
      </div>
    )
  }
}
​
const App = () => {
  const btnRef = useRef()
  const comRef = useRef()
  const funRef = useRef()
​
  useEffect(() => {
    // console.log(btnRef)
    btnRef.current.style.color = 'red'
    console.log(comRef.current)
    // console.log(funRef.current)
    funRef.current.value = '睡觉大王-刘创'
​
  }, [])
​
  return (
    <div>
      <button ref = { btnRef }>按钮</button>
      <Com ref = { comRef }/>
      <Fun ref = { funRef }/>
    </div>
  );
};
​
export default App;

17.2.4 useReducer

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

1、创建初始值initialState

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

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

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

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
import App from './12_hooks/06App_useReducer'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/06App_useReducer.jsx

// src/12_hooks/06App_useReducer.jsximport React, { useEffect, useReducer } from 'react';
​
// 1.定义组件的初始化状态
const intialState = {
  bannerList: [],
  proList: []
}
// 2.创建所有操作 reducer(state, action); 定义好修改状态的reducer - 纯函数
// state 初始化状态以及上一次的状态
// action 包含修改状态的标识以及传递的数据 { type: '', payload: '' }
const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_BANNER_LIST': // 标识 触发 dispatch({ type: 'CHANGE_BANNER_LIST', payload: '' })
      return Object.assign({}, state, { bannerList: action.payload })
    case 'CHANGE_PRO_LIST': // 标识
      return { ...state, proList: action.payload }
    default:
      return state
  }
}
​
const App = () => {
  // 3.传给 userReducer,得到读和写API
  const [state, dispatch] = useReducer(reducer, intialState)
​
  useEffect(() => {
    fetch('http://121.89.205.189:3000/api/banner/list')
      .then(res => res.json())
      .then(res => {
        dispatch({
          type: 'CHANGE_BANNER_LIST',
          payload: res.data
        })
      })
    fetch('http://121.89.205.189:3000/api/pro/list')
      .then(res => res.json())
      .then(res => {
        dispatch({
          type: 'CHANGE_PRO_LIST',
          payload: res.data
        })
      })
  }, [])
  return (
    <div>
      <h1>banner</h1>
      {
        state.bannerList && state.bannerList.map(item => {
          return <img src={ item.img } alt="" key={item.bannerid} style={{ height: 60 }} />
        })
      }
      <h1>pro</h1>
      {
        state.proList && state.proList.map(item => {
          return (
            <div key={ item.proid }>
              <img src={ item.img1 } alt="" style={{ height: 60 }} />
              { item.proname }
            </div>
          )
        })
      }
    </div>
  );
};
​
export default App;

如果遇到多个组件需要共享状态时,单纯useReducer就显得无能为力

17.2.5 useContext

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

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

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
import App from './12_hooks/07App_useContext'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/01App_useContext.jsx

// src/12_hooks/01App_useContext.jsx
import React, { useContext } from 'react';
const MyContext = React.createContext()
​
const Child = () => {
  const val = useContext(MyContext)
​
  return (
    <div>child - { val }</div>
  )
}
const App = () => {
  return (
    <div>
      <MyContext.Provider value = '传家宝'>
        <Child />
      </MyContext.Provider>
    </div>
  );
};
​
export default App;

使用 useReducer 和 useContext 实现轻型Redux,可以让组件间共享状态

步骤:

1、将数据集中在一个 store 对象

2、将所有操作集中在 reducer

3、创建一个 Context

4、创建对数据的读取 API

5、将第四步的内容放到第三步的 Context

6、用 Context.Provider 将 Context 提供给所有组件

7、各个组件用 useContext 获取读写API

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
import App from './12_hooks/08App_little_redux'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/08App_little_redux.jsx

// src/12_hooks/08App_little_redux.jsx
import React, { useContext, useEffect, useReducer } from 'react';
// 4.
const MyContext = React.createContext()
​
// 1.
const intialState = {
  bannerList: [],
  proList: []
}
// 2.
const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_BANNER_LIST':
      return Object.assign({}, state, { bannerList: action.payload })
    case 'CHANGE_PRO_LIST': 
      return { ...state, proList: action.payload }
    default:
      return state
  }
}
​
function Home () {
  // 6.
  const {state, dispatch} = useContext(MyContext)
​
  useEffect(() => {
    fetch('http://121.89.205.189:3000/api/banner/list')
      .then(res => res.json())
      .then(res => {
        // 7.
        dispatch({
          type: 'CHANGE_BANNER_LIST',
          payload: res.data
        })
      })
    fetch('http://121.89.205.189:3000/api/pro/list')
      .then(res => res.json())
      .then(res => {
        dispatch({
          type: 'CHANGE_PRO_LIST',
          payload: res.data
        })
      })
  }, [])
  return (
    <div>
      首页
      
    </div>
  )
}
​
function List () {
  // 8.
  const {state} = useContext(MyContext)
  return (
    <div>
      列表
      {/* 9. */}
      {
        state.bannerList && state.bannerList.map(item => {
          return <img src={ item.img } alt="" key={item.bannerid} style={{ height: 60 }} />
        })
      }
      <h1>pro</h1>
      {
        state.proList && state.proList.map(item => {
          return (
            <div key={ item.proid }>
              <img src={ item.img1 } alt="" style={{ height: 60 }} />
              { item.proname }
            </div>
          )
        })
      }
    </div>
  )
}
const App = () => {
  // 3.
  const [state, dispatch] = useReducer(reducer, intialState)
​
  
  return (
    <div>
      {/* 5. */}
      <MyContext.Provider value = {{ state, dispatch }}>
        <Home />
​
        <hr />
​
        <List />
      </MyContext.Provider>
      
    </div>
  );
};
​
export default App;

useContext + useReducer 可以实现轻型redux,但是不适合应用于多模块管理的大型项目

17.2.6 useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。

17.2.7 useCallback

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// 引入react组件时,后缀名可以不写
// import App from './12_hooks/01_App_hooks_useState'
// import App from './12_hooks/02_App_hooks_useState_obj'
// import App from './12_hooks/03_App_hooks_useState_state'
// import App from './12_hooks/04_App_hooks_useEffect'
// import App from './12_hooks/05_App_hooks_useRef'
// import App from './12_hooks/06_App_hooks_useReducer'
// import App from './12_hooks/07_App_hooks_useContext'
// import App from './12_hooks/08_App_hooks_redux'
import App from './12_hooks/09_App_hooks_useCallback_useMemo'const root = ReactDOM.createRoot(document.querySelector('#root'))
​
root.render(<App />)
​

src/12_hooks/09_App_hooks_useCallback_useMemo.jsx

// src/12_hooks/09App_useCallback_useMemo.jsx
import React, { memo, useCallback, useMemo, useState } from 'react';
// 如果子组件是函数式组件,且调用组件时没有传递任何依赖项
// 想要达到子组件不重新渲染,需要使用高阶组件 React.memo 包裹组件const Child = memo(() => {
  console.log('child1')
  return (
    <div>child</div>
  )
})
const App = () => {
  const [arr] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9])
  const [count, setCount] = useState(0)
  const [a, setA] = useState(10)
​
  const addA = () => {
    setA(a + 1)
  }
  // const handler = (event) => {} // 只要count的值发生改变,那么child组件就会被重新渲染 --- 每次都产生一个新的函数
  
  // const handler = useCallback(() => { 
  //   return (event) => {} 
  // }, []) // 记忆函数 (event) => {}
  const handler = useMemo((event) => {}, [])
​
  // 计算属性
  const len = useMemo(() => { 
    return arr.length 
  }, [arr])
  return (
    <div>
      { count }
      <button onClick={() => setCount(count + 1)}>加1</button>
      <button onClick={addA}>a加1</button> - { len }
      <Child a={a} handler = { handler }/>
    </div>
  );
};
​
export default App;

useCallback 应用于 组件的事件,使用useCallback 包裹组件 - 记忆函数

useMemo 可以是计算属性, 也应用于 组件的事件,使用useMemo 包裹组件 - 记忆组件

17.2.8 useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
import App from './12_hooks/11App_useImperativeHandle'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/11App_useImperativeHandle.jsx

// src/12_hooks/11App_useImperativeHandle.jsx
// 在父组件中获取子组件提供的数据何方法
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
​
const MyInput = forwardRef((props, ref) => {
  const inputRef = useRef()
​
  // 关联了父组件传递过来的 ref 值
  // 暴露了父组件可以访问的子组件的数据何方法
  useImperativeHandle(ref, () => {
    return {
      a: '111', 
      b: 222,
      focus: () => {
        inputRef.current.focus()
      }
    }
  })
​
  return (
    <div>
      <input type="text" ref = { inputRef }/>
    </div>
  )
})
​
const App = () => {
  const myInputRef = useRef()
  const getData = () => {
    console.log(myInputRef.current.a)
    console.log(myInputRef.current.b)
    myInputRef.current.focus()
  }
  return (
    <div>
      <button onClick={ getData }>获取子组件的数据以及获取焦点</button>
      <MyInput ref = { myInputRef }/>
    </div>
  );
};
​
export default App;

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

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

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

17.2.9 useLayoutEffect

useLayoutEffect 与 useEffect的区别:

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

举个例子:

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
import App from './12_hooks/12App_useLayoutEffect'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12App_useLayoutEffect.jsx

// src/12_hooks/12App_useLayoutEffect.jsximport React, { useEffect, useLayoutEffect, useRef } from 'react';
​
const App = () => {
  const inputRef = useRef()
  useEffect(() => { // DOM操作 数据请求
    console.log('useEffect') // 后打印
    inputRef.current.value = '1111'
  }, [])
  useLayoutEffect(() => { // DOM操作
    console.log('useLayoutEffect') // 先打印
    inputRef.current.value = '2222'
  })
  return (
    <div>
      <input type="text" ref={ inputRef } />
    </div>
  );
};
​
export default App;

useLayoutEffect 做DOM操作

useEffect 中 副作用执行

17.2.10 useDebugValue

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
import App from './12_hooks/13App_useDebugValue'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/13App_useDebugValue.jsx

// src/12_hooks/13App_useDebugValue.jsx
import React, { useDebugValue, useEffect, useState } from 'react';
​
// const App = () => {//   const [count, setCount] = useState(0)
//   const add = () => setCount(count + 1)//   useEffect(() => {
//     document.title = `点击了${count}次`
//   })
//   return (
//     <div>
//       <button onClick={ add }>加1</button> { count }
//     </div>
//   );
// };const useCount = () => {
  useDebugValue('myCount')
  const [count, setCount] = useState(0)
  const add = () => setCount(count + 1)
  return {
    count, add
  }
}
const useTitle = (count) => {
  useDebugValue('myTitle')
  useEffect(() => {
    document.title = `点击了${count}次`
  })
}
const App = () => {
  const { count, add } = useCount()
  useTitle(count)
  return (
    <div>
      <button onClick={ add }>加1</button> { count }
    </div>
  );
} 
​
export default App;

接下来的hooks是属于react18版本新增的hooks

17.2.11 useId

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

注意:

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

不能用于列表渲染的key值

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

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
import App from './12_hooks/14App_useId'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/14App_useId.jsx

// src/12_hooks/14App_useId.jsx
import React, { useId, useState } from 'react';
​
const App = () => {
  const [list] = useState(['aa', 'bb', 'cc'])
  const useNameId = useId()
  const passwordId = useId()
  return (
    <div>
      {
        // list.map(item => { // ❌
        //   const id = useId() // 不可以在jsx代码中使用 hooks
        //   return <p key={ id }>{ item }</p>
        // })
        list.map(item => {
          return <p key={ item }>{ item }</p>
        })
      }
​
      <form >
        <div>
          {/* 网页中为 for属性 ==》 react  htmlFor */}
          {/* <label htmlFor="userName">用户名</label>
          <input type="text" id='userName' /> */}
​
          <label htmlFor={ useNameId }>用户名</label>
          <input type="text" id={ useNameId } />
        </div>
        <div>
          <label htmlFor={ passwordId }>密码</label>
          <input type="password" id={ passwordId } />
        </div>
      </form>
    </div>
  );
};
​
export default App;

注意:

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

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

hooks需要在函数式组件以及自定义hook的顶级使用(返回jsx代码之前),不要在jsx代码中使用hooks

17.2.12 useDeferredValue - 暂时了解

真实需求其实不需要实时渲染所有的数据

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
// import App from './12_hooks/14App_useId'
import App from './12_hooks/15App_useDeferredValue'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/15App_useDeferredValue.jsx 数据量必须过大,否则看不到效果

// src/12_hooks/15App_useDeferredValue.jsx
import React, { useDeferredValue, useEffect, useState, useMemo, memo } from 'react'
const List = memo(function List({count}) {
  const [data, setData] = useState([])
​
  useEffect(() => {
    const data = []
    data.length = 50000
    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>
  )
})
​
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="text" value={inpVal}  onChange={(e) => setInpVal(e.target.value)}/>
      {memoList}
    </>
  )
}

17.2.13 useTransition - 暂时了解

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

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

src/index.js

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'// import App from './12_hooks/01App_useState'
// import App from './12_hooks/02App_useState'
// import App from './12_hooks/03App_useState'
// import App from './12_hooks/04App_useEffect'
// import App from './12_hooks/05App_useRef'
// import App from './12_hooks/06App_useReducer'
// import App from './12_hooks/07App_useContext'
// import App from './12_hooks/08App_little_redux'
// import App from './12_hooks/09App_useCallback_useMemo'
// import App from './12_hooks/10App_useCallback_useMemo'
// import App from './12_hooks/11App_useImperativeHandle'
// import App from './12_hooks/12App_useLayoutEffect'
// import App from './12_hooks/13App_useDebugValue'
// import App from './12_hooks/14App_useId'
// import App from './12_hooks/15App_useDeferredValue'
import App from './12_hooks/16App_useTransition'import ErrorBoundary from './ErrorBoundary'// import './style.stylus'const root = ReactDOM.createRoot(document.getElementById('root'))
​
root.render(
  <ErrorBoundary>
    {/* react严格模式的组件 */}
    {/* <React.StrictMode> */}
      <App root={ root }/>
    {/* </React.StrictMode> */}
  </ErrorBoundary>
)
​

src/12_hooks/16App_useTransition

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>
  )
}
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>
  )
}

17.2.14 useSyncExternalStore - 暂时了解

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

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

1、设计store

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

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

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

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 同步组件状态

import React, { useSyncExternalStore } from 'react'
import store from './store'export default function UseSyncExternalStoreDemo() {
  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>
    )
}

17.2.15 useInsertionEffect - 了解

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

import React, { useInsertionEffect, useEffect, useLayoutEffect } from 'react'
 
export default function UseInsertionEffect() {
​
  useInsertionEffect(() => {
    console.log('useInsertionEffect') // 1
    // const style = document.createElement('style')
    // style.innerHTML = '.box { color: red }'
    // document.head.appendChild(style)
  })
​
  useEffect(() => {
    console.log('useEffect') // 3
  })
​
  useLayoutEffect(() => {
    console.log('useLayoutEffect') // 2
  })
​
  return (
    <div className="box">UseInsertionEffect</div>
  )
}

useEffect - useLayoutEffect - useInsertionEffect

17.3 自定义hooks

以use开头的小驼峰式的函数