一、react新特性
1. context
在一個典型的 React 應(yīng)用中,數(shù)據(jù)是通過 props 屬性自上而下(由父及子)進行傳遞的,但這種做法對于某些類型的屬性而言是極其繁瑣的(例如:地區(qū)偏好,UI 主題),這些屬性是應(yīng)用程序中許多組件都需要的。Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞 props。
來看一個例子,首先來創(chuàng)建一個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)
現(xiàn)在這些值就可以在組件中使用了。
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 非常相似,但它適用于函數(shù)組件,但不適用于 class 組件。
如果你的函數(shù)組件在給定相同 props 的情況下渲染相同的結(jié)果,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。
默認情況下其只會對復(fù)雜對象做淺層對比,如果你想要控制對比過程,那么請將自定義的比較函數(shù)通過第二個參數(shù)傳入來實現(xiàn)。
二、項目配置
1. eslint配置
使用eslint hooks可以幫我們檢查使用hooks過程中出現(xiàn)的錯誤,使用步驟如下:
安裝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多頁應(yīng)用的webpack配置
我們使用create-react-app腳手架生成項目,然后對配置內(nèi)容做一些改動,使它支持多頁應(yīng)用。
首先在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中設(shè)置多入口打包
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),
}
接下來對每個頁面設(shè)置模版,這里以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
)
)
我們還可以設(shè)置打包分析,在webpack.config.js中引入webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 打包分析
// 添加到plugins中
process.env.GENERATE_BUNDLE_ANALYZER === 'true' &&
new BundleAnalyzerPlugin({
openAnalyzer: false, // 禁止打開服務(wù)器
analyzerMode: 'static', // 生成靜態(tài)html文件
})
還可以設(shè)置cdn路徑,這里有兩種方法,一種是我們在執(zhí)行打包命令時添加cdn地址:
PUBLIC_URL=https://www.cdn.baidu.com/ npm run build
第二種需要我們修改配置文件
當我們在output中設(shè)置publicPath時,根據(jù)環(huán)境設(shè)置不同的路徑
publicPath: 'production' !== process.env.NODE_ENV || 'true' === process.env.USE_LOCAL_FILES
? '/'
: 'https://www.cdn.baidu.com/',
設(shè)置代理,我們在package.json中配置
"proxy": "http://localhost:5555",
三、 redux
之前的redux創(chuàng)建可參考react進階
我們來創(chuàng)建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個參數(shù):reducer, preloadedState, enhancer。第二個參數(shù)是preloadedState,它是state的初始值,如果我們第二個參數(shù)是函數(shù)類型,createStore會認為你忽略了preloadedState而傳入了一個enhancer,如果我們傳入了一個enhancer,createStore會返回enhancer(createStore)(reducer, preloadedState)的調(diào)用結(jié)果,這是常見高階函數(shù)的調(diào)用方式。在這個調(diào)用中enhancer接受createStore作為參數(shù),對createStore的能力進行增強,并返回增強后的createStore。然后再將reducer和preloadedState作為參數(shù)傳給增強后的createStore,最終得到生成的store。
redux-thunk的作用是使redux支持異步action(先獲取當前值,再做一些修改)
Redux中負責響應(yīng)action并修改數(shù)據(jù)的角色就是reducer,reducer的本質(zhì)實際上是一個函數(shù),其函數(shù)簽名為:reducer(previousState,action) => newState??梢钥闯觯瑀educer 接受兩個參數(shù),previousState以及action函數(shù)返回的action對象,并返回最新的state。來創(chuàng)建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
},
}
最后來設(shè)置action,action代表的是用戶的操作。redux規(guī)定action一定要包含一個type屬性,且type屬性也要唯一,相同的type,redux視為同一種操作,因為處理action的函數(shù)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轉(zhuǎn)化為dispatch(action)的函數(shù)集合形式。開發(fā)者不用再手動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
在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱、設(shè)置定時器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因為這可能會產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。
我們需要使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會在組件渲染到屏幕之后執(zhí)行。
組件卸載時需要清除 effect 創(chuàng)建的諸如訂閱或計時器 ID 等資源。要實現(xiàn)這一點,useEffect 函數(shù)需返回一個清除函數(shù)
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)
}
}, []) // 數(shù)組中的每一項都不變,useEffect才不會執(zhí)行
return (
<button
type="button"
onClick={() => {setCount(count+1)}}
>
Click({count})
size: {size.width}X{size.height}
</button>
);
}
effect 的執(zhí)行時機
與 componentDidMount、componentDidUpdate 不同的是,在瀏覽器完成布局與繪制之后,傳給 useEffect 的函數(shù)會延遲調(diào)用。這使得它適用于許多常見的副作用場景,比如如設(shè)置訂閱和事件處理等情況,因此不應(yīng)在函數(shù)中執(zhí)行阻塞瀏覽器更新屏幕的操作。
然而,并非所有 effect 都可以被延遲執(zhí)行。例如,在瀏覽器執(zhí)行下一次繪制前,用戶可見的 DOM 變更就必須同步執(zhí)行,這樣用戶才不會感覺到視覺上的不一致。(概念上類似于被動監(jiān)聽事件和主動監(jiān)聽事件的區(qū)別。)React 為此提供了一個額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結(jié)構(gòu)相同,區(qū)別只是調(diào)用時機不同。
雖然 useEffect 會在瀏覽器繪制后延遲執(zhí)行,但會保證在任何新的渲染前執(zhí)行。React 將在組件更新前刷新上一輪渲染的 effect。
3. useContext
之前context時用法以介紹郭,可翻看上面。
4. useMemo
useMemo(() => fn) === useCallback(fn)
useMemo和useCallback都會在組件第一次渲染的時候執(zhí)行,之后會在其依賴的變量發(fā)生改變時再次執(zhí)行;并且這兩個hooks都返回緩存的值,useMemo返回緩存的變量,useCallback返回緩存的函數(shù)
參考: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]) // 空數(shù)組代表只執(zhí)行一次
return (
<div>
<button
type="button"
onClick={() => {setCount(count+1)}}
>
Click({count}) {double}
</button>
<Counter count={count}/>
</div>
);
}
5. useReducer
在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復(fù)雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù)
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>
</>
);
}
惰性初始化(異步),只要把第三個參數(shù)轉(zhuǎn)為一個函數(shù)即可
function init(initialCount) {
return {count: initialCount};
}
const [state, dispatch] = useReducer(reducer, initialCount, init)
6. useRef
- 一個常見的用例便是命令式地訪問子組件:
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。
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 中使用實例字段的方式。
這是因為它創(chuàng)建的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...}對象的唯一區(qū)別是,useRef 會在每次渲染時返回同一個 ref 對象。
6. useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父組件的實例值。在大多數(shù)情況下,應(yīng)當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應(yīng)當與 forwardRef 一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染 <FancyInput ref={fancyInputRef} /> 的父組件可以調(diào)用 fancyInputRef.current.focus()。
7. useLayoutEffect
其函數(shù)簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調(diào)用 effect??梢允褂盟鼇碜x取 DOM 布局并同步觸發(fā)重渲染。在瀏覽器執(zhí)行繪制之前,useLayoutEffect 內(nèi)部的更新計劃將被同步刷新。
盡可能使用標準的 useEffect 以避免阻塞視覺更新。
8. hooks使用規(guī)則
不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook
確保總是在你的 React 函數(shù)的最頂層調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的 useState 和 useEffect 調(diào)用之間保持 hook 狀態(tài)的正確。
只在 React 函數(shù)中調(diào)用 Hook
不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook。你可以:
- ? 在 React 的函數(shù)組件中調(diào)用 Hook
- ? 在自定義 Hook 中調(diào)用其他 Hook