一、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"
}
如下图所示:
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 的执行时机
与 componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给 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
- 一个常见的用例便是命令式地访问子组件:
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>
</>
);
}
- 然而,
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 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook