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'
<TrainContext.Provider
   value={{
      trainNumber,
      departStation,
      arriveStation,
      departDate,
    }}
>
  <Candidate tickets={tickets} />
</TrainContext.Provider>

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 (
    <div>
      <Suspense fallback={<div>loading</div>}>
        <About />
      </Suspense>
    </div>
  );
}

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

如下图所示:


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 (
    <button
      type="button"
      onClick={() => {setCount(count+1)}}
    >
      Click({count})
    </button>

  );
}

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 (
    <button
      type="button"
      onClick={() => {setCount(count+1)}}
    >
      Click({count})
      size: {size.width}X{size.height}
    </button>
  );
}

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 (
    <h1>{props.count}</h1>
  )
}

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

  return (
    <div>
      <button
      type="button"
      onClick={() => {setCount(count+1)}}
      >
        Click({count}) {double}
      </button>
      <Counter count={count}/>
    </div>
  );
}

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}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

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

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 (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  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 <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={fancyInputRef} /> 的父组件可以调用 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348