react hooks使用指北

一、react新特性

1. context

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

来看一个例子,首先来创建一个context

// context.js
import { createContext } from 'react'

export const TrainContext = createContext()

然后在app.jsx中引入

// app.jsx
import { TrainContext } from './context'

  

channel是candidate的一个子组件,我们在channel中通过useContext获取context中的值

const {
    trainNumber,
    departStation,
    arriveStation,
    departDate,
  } = useContext(TrainContext)

现在这些值就可以在组件中使用了。

2. 异步加载组件

我们配合Suspense使用

import React, { lazy, Suspense } from 'react';

const About = lazy(() => import('./About')) // 延迟(异步)加载

function App() {
  return (
    
loading
}>
); }

3. Memo

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但不适用于 class 组件。

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

二、项目配置

1. eslint配置

使用eslint hooks可以帮我们检查使用hooks过程中出现的错误,使用步骤如下:
安装eslint-plugin-react-hooks

npm install eslint-plugin-react-hooks --save-dev

然后在package.json中添加

"plugins": [
      "react-hooks"
],
"rules": {
      "react-hooks/rules-of-hooks": "error"
}

如下图所示:


react hooks使用指北_第1张图片
image.png

2. react多页应用的webpack配置

我们使用create-react-app脚手架生成项目,然后对配置内容做一些改动,使它支持多页应用。
首先在path.js中添加多页的路径和模版html路径

  appHtml: resolveApp('public/index.html'),
  appQueryHtml: resolveApp('public/query.html'),
  appOrderHtml: resolveApp('public/order.html'),
  appTicketHtml: resolveApp('public/ticket.html'),
  appIndexJs: resolveModule(resolveApp, 'src/index/'),
  appOrderJs: resolveModule(resolveApp, 'src/order/'),
  appQueryJs: resolveModule(resolveApp, 'src/query/'),
  appTicketJs: resolveModule(resolveApp, 'src/ticket/'),

然后在entry中设置多入口打包

entry: {
      index: [paths.appIndexJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      qusry: [paths.appQueryJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      ticket: [paths.appTicketJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      order: [paths.appOrderJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
}

接下来对每个页面设置模版,这里以query.html为例

new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appQueryHtml,
            filename: 'query.html',
            chunks: ['query']
          },
          isEnvProduction
            ? {
                minify: {
                  removeComments: true,
                  collapseWhitespace: true,
                  removeRedundantAttributes: true,
                  useShortDoctype: true,
                  removeEmptyAttributes: true,
                  removeStyleLinkTypeAttributes: true,
                  keepClosingSlash: true,
                  minifyJS: true,
                  minifyCSS: true,
                  minifyURLs: true,
                },
              }
            : undefined
      )
)

我们还可以设置打包分析,在webpack.config.js中引入webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 打包分析

// 添加到plugins中
process.env.GENERATE_BUNDLE_ANALYZER === 'true' &&
        new BundleAnalyzerPlugin({
          openAnalyzer: false, // 禁止打开服务器
          analyzerMode: 'static', // 生成静态html文件
        })

还可以设置cdn路径,这里有两种方法,一种是我们在执行打包命令时添加cdn地址:

PUBLIC_URL=https://www.cdn.baidu.com/ npm run build

第二种需要我们修改配置文件
当我们在output中设置publicPath时,根据环境设置不同的路径

publicPath: 'production' !== process.env.NODE_ENV || 'true' === process.env.USE_LOCAL_FILES
          ? '/'
          : 'https://www.cdn.baidu.com/',

设置代理,我们在package.json中配置

"proxy": "http://localhost:5555",

三、 redux

之前的redux创建可参考react进阶
我们来创建store.js

import { createStore, combineReducers, applyMiddleware } from 'redux'

import reducers from './reducers'
import thunk from 'redux-thunk'

export default createStore(
  combineReducers(reducers),
  {
    departDate: Date.now(),
    arriveDate: Date.now(),
    departTimeStr: null, // 到达时间小时
    arriveTimeStr: null,
  },
  applyMiddleware(thunk)
)

createStore接受3个参数:reducer, preloadedState, enhancer。第二个参数是preloadedState,它是state的初始值,如果我们第二个参数是函数类型,createStore会认为你忽略了preloadedState而传入了一个enhancer,如果我们传入了一个enhancer,createStore会返回enhancer(createStore)(reducer, preloadedState)的调用结果,这是常见高阶函数的调用方式。在这个调用中enhancer接受createStore作为参数,对createStore的能力进行增强,并返回增强后的createStore。然后再将reducer和preloadedState作为参数传给增强后的createStore,最终得到生成的store。

redux-thunk的作用是使redux支持异步action(先获取当前值,再做一些修改)

Redux中负责响应action并修改数据的角色就是reducer,reducer的本质实际上是一个函数,其函数签名为:reducer(previousState,action) => newState。可以看出,reducer 接受两个参数,previousState以及action函数返回的action对象,并返回最新的state。来创建reducers.js

export default {
  departDate(state = Date.now(), action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_DEPART_DATE:
        return payload
      default:
    }

    return state
  },
  arriveDate(state = Date.now(), action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_ARRIVE_DATE:
        return payload
      default:
    }

    return state
  },
  departTimeStr(state = null, action) {
      const { type, payload } = action
      switch (type) {
        case ACTION_SET_DEPART_TIME_STR:
          return payload
        default:
      }

      return state
  },
  arriveTimeStr(state = null, action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_ARRIVE_TIME_STR:
        return payload
      default:
    }

    return state
  },
}

最后来设置action,action代表的是用户的操作。redux规定action一定要包含一个type属性,且type属性也要唯一,相同的type,redux视为同一种操作,因为处理action的函数reducer只判断action中的type属性。新建actions.js

export const ACTION_SET_DEPART_DATE = 'SET_DEPART_DATE'
export const ACTION_SET_ARRIVE_DATE = 'SET_ARRIVE_DATE'
export const ACTION_SET_DEPART_TIME_STR = 'SET_DEPART_TIME_STR'
export const ACTION_SET_ARRIVE_TIME_STR = 'SET_ARRIVE_TIME_STR'

export function setDepartDate(departDate) {
  return {
    type: ACTION_SET_DEPART_DATE,
    payload: departDate,
  }
}
export function setArriveDate(arriveDate) {
  return {
    type: ACTION_SET_ARRIVE_DATE,
    payload: arriveDate,
  }
}
export function setDepartTimeStr(departTimeStr) {
  return {
    type: ACTION_SET_DEPART_TIME_STR,
    payload: departTimeStr,
  }
}
export function setArriveTimeStr(arriveTimeStr) {
  return {
    type: ACTION_SET_ARRIVE_TIME_STR,
    payload: arriveTimeStr,
  }
}

// 异步action写法
export function nextDate() {
  return (dispatch, getState) => {
    const { departDate } = getState()

    dispatch(setDepartDate(h0(departDate) + 86400 * 1000))
  }
}

然后我们在组件中引入

import { bindActionCreators } from 'redux'

import {
  exchangeFromTo,
  showCitySelector,
} from './actions'

function App(props) {
  const cbs = useMemo(() => {
    return bindActionCreators(
      {
        exchangeFromTo,
        showCitySelector,
      }, dispatch)
  }, [])
}

export default connect(
  function mapStateToProps(state) {
    return state
  },
  function mapDispatchToProps(dispatch) {
    return { dispatch }
  }
)(App)

bindActionCreators是redux的一个API,作用是将单个或多个ActionCreator转化为dispatch(action)的函数集合形式。开发者不用再手动dispatch(actionCreator(type))

四、React hooks

1. useState

function App(props) {
  const [count, setCount] = useState(() => { // 只运行一次
    return props.defaultCount || 0 // 返回值就是usestate默认值
  })
  return (
    

  );
}

2. useEffect

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

我们需要使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数

function App(props) {
  const [count, setCount] = useState(() => {
    return props.defaultCount || 0
  })
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  })
  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    })
  }
  useEffect(() => {
    document.title = count
  })
  useEffect(() => {
    window.addEventListener("resize", onResize, false)
    return () => { // 解绑
      window.removeEventListener("resize", onResize, false)
    }
  }, []) // 数组中的每一项都不变,useEffect才不会执行
  return (
    
  );
}

effect 的执行时机
componentDidMountcomponentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

3. useContext

之前context时用法以介绍郭,可翻看上面。

4. useMemo

useMemo(() => fn) === useCallback(fn)

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数

参考:https://blog.csdn.net/sinat_17775997/article/details/94453167

function Counter(props) {
  return (
    

{props.count}

) } function App(props) { const [count, setCount] = useState(() => { return props.defaultCount || 0 }) const double = useMemo(() => { return count*2 }, [count]) // 空数组代表只执行一次 return (
); }

5. useReducer

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      
      
    
  );
}

惰性初始化(异步),只要把第三个参数转为一个函数即可

function init(initialCount) {
  return {count: initialCount};
}
const [state, dispatch] = useReducer(reducer, initialCount, init)

6. useRef

  1. 一个常见的用例便是命令式地访问子组件:
    useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      
      
    
  );
}
  1. 然而,useRef()ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...}对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

6. useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

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

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return ;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 的父组件可以调用 fancyInputRef.current.focus()

7. useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

8. hooks使用规则

不要在循环,条件或嵌套函数中调用 Hook
确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:

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

你可能感兴趣的:(react hooks使用指北)