React自發布v16版本以來已經有半年了,至今最新的是v16.3。從 v16 開始增長了較多新的API。相較於以前純淨的API設計,變化能夠說是很是大了。能夠看出 facebook 的 React 團隊已經解決了以前的大多數問題,如今開始爲 React 設計新的 API 、增長新功能了。得益於重寫 React 底層爲Fiber架構,v16 包含了許多實用的新特性,而且也有一些 「break-change」,以後的版本確定更多。在官方博客上的介紹雖然很全,可是因爲翻譯不及時,查閱不變,並且有的地方難於理解。故在此經過一些代碼作簡要介紹,也爲本身查閱 API 作記錄。html
倉庫中代碼包含了本人的最佳實踐,閱讀文章以前, 但願你對 React 和 ES6 有必定了解。固然若是不會,也能夠選擇性看看。若有例子不合適或謬誤,歡迎 提出意見。前端
代碼已託管到 github倉庫 上,裏面有對應新特性子的 demo,歡迎 star !沒錯我就是來騙讚的react
因爲準備長期更新,如今的是 v16 版。因此克隆到本地之後先 cd react16
,再npm i
, npm start
。訪問 localhost:9000 便可看到 demo(忽略我醜陋的 CSS )。左邊有幾個路由,分別對應了幾個新特性和相應的 demo。git
React v16 以前,若是渲染中出現錯誤,整個頁面會直接崩掉。若是對 React 足夠了解,可能會知道一個祕而不宣的API: unstable_handleError
。此函數可用於捕獲頁面錯誤,然而因爲文檔沒記錄,知道的開發者也寥寥無幾。如今咱們有了新的、官方的、穩定的 API: componentDidCatch
。就像 try catch
同樣,可用於捕獲 render
過程當中的錯誤,經常使用於捕獲錯誤並渲染不一樣的頁面,避免整個頁面崩潰。github
demo 中,第一個是自增按鈕,增長到5會拋出渲染錯誤,若是此時處於不捕獲模式,頁面崩潰,只能刷新頁面恢復正常。npm
相信這是每個 React 頁面仔的噩夢吧(包括我),應該盡力避免此種狀況發生。切換到捕獲模式後,組件啓用新的componentDidCatch
API。此時發生錯誤不會崩,會顯示備用頁面。redux
新特性的主要代碼在 /src/ErrorBoundary/ErrorHandler.jsx
下。原本若是不用切換模式,一直捕獲錯誤的話,ErrorHander
應該長成這樣:api
import React from 'react'
class FakeHandler extends React.Component {
state = {
hasError: false
}
// 新的生命週期鉤子
componentDidCatch(error, info) {
this.setState({
hasError: true
})
}
// 重置狀態,與新 API 無關
reset = () => {
this.setState({
hasError: false
})
this.props.reset()
}
render() {
// 顯示備用頁面的核心代碼,如有錯誤顯示備用頁面
return this.state.hasError ? (
<React.Fragment> <p>頁面渲染髮生錯誤,這是備用頁面,可打開 console 查看錯誤</p> <div> <button onClick={this.reset}>點擊此處重置</button> </div> </React.Fragment> ) : ( React.Children.only(this.props.children) ) } } 複製代碼
重點在 componentDidCatch
這一句,若是捕獲錯誤了把當前state.hasError
設爲true
,render
裏判斷下是否有錯誤再渲染,能夠作到備用頁面的顯示,這也是經常使用的 componentDidCatch
處理手法,能夠做爲經典範例。數組
componentDidCatch
能夠接受兩個參數: 拋出的錯誤error
和 錯誤信息的 info
,如今的info只包含了調用棧的信息,感受用處不大,由於發生錯誤時React老是會打印堆棧。可能之後會加入新信息,拭目以待。架構
因爲此方法能夠放在任意組件內,所以能夠在頁面不一樣地方定製化備用頁。
Note:今生命週期函數沒法捕獲渲染外的錯誤,如如下錯誤沒法捕獲,會正常渲染。
class A extends React.Component {
render() {
// 此錯誤沒法被捕獲,渲染時組件正常返回 `<div></div>`
setTimeout(() => {
throw new Error('error')
}, 1000)
return (
<div></div>
)
}
}
複製代碼
倉庫中的代碼因爲須要切換捕獲模式以演示區別,所以出現了組件的繼承寫法,若是不熟悉,請好好體會。不過實際項目中遇到繼承的機會仍是不多的,此種方法經常使用於覆蓋某組件的生命週期函數。
官網文檔已有中文版,更多詳情請參閱 Error Boundaries
API爲 ReactDOM.createPortal
。能夠簡單的理解爲「傳送門」,便可以直接渲染在父組件之外的任意 DOM 節點,經常使用於彈出框、提示框等,而且支持事件冒泡,行爲徹底與子組件一致。demo 代碼在src/Portal
下。注意此方法並不能爲所欲爲調用,只有在組件的 render 方法調用,並做爲合法element
的代替返回。
Note: 新的 API 掛載在 react-dom 下,並非 React 包內。
代碼示例:
import React from 'react'
import { createPortal } from 'react-dom'
class Dialog extends React.Component {
render() {
// 必定要 return
return createPortal((
<div></div>
), document.querySelector('#dialog'))
}
}
複製代碼
渲染的實際 DOM 如圖,即便整個應用都在 div#app
下,createPortal 依然能在以外的 div#poral
下渲染 Element。
很是簡單,可是要注意不能濫用,就像 ref 同樣,儘可能把 react 能作的都交給 react 處理。淡然此 API 作彈出框的時候很是好用,對作基本彈窗組件的前端們簡直就是福音。更多請參考官方中文文檔 Portals。
這兩個靜態組件均掛載在 React 包下,經過React.Fragment
和React.StrictMode
可訪問到。
Fragment靜態組件,v16.0 推出,用於將多個 React render
返回值包裹成一個具備頂級元素的element
。以前若是咱們須要返回多個元素,必定要在外面包一層<div></div>
或其餘的元素,React 還會將其渲染成真實 DOM;或直接返回一個相應的數組(React v16.0支持),可是很是醜陋,而且必須附帶key
屬性,即便用不到。
如今新的 Fragment 僅用於包裹,並不會生成對應 DOM 了,就像普通的jsx同樣,也不須要key
屬性了,仍是很是不錯的新功能。官方文檔:Fragments
StrictMode 於 v16.3 推出。顧名思義,即嚴格模式,可用於在開發環境下提醒組件內使用不推薦寫法和即將廢棄的 API(該版本廢棄了三個生命週期鉤子)。與 Fragment 相同,並不會被渲染成真實 DOM。官方文檔嚴格模式裏詳細介紹了會在哪些狀況下發出警告。對於咱們開發者來講,及時棄用不被推薦的寫法便可規避這些警告。
Fragment 和 StrictMode 代碼示例在src/NewComponent
下:
import React, {Fragment, StrictMode} from 'react'
const FragmentItem = props => new Array(5).fill(null).map((k, i) => (
<Fragment key={i}> <p>這是第{i}項</p> <p>{i} * {i} = {i * i}</p> </Fragment>
))
class OldLifecycleProvider extends React.Component {
// 如下三個函數在 React v16.3 已不被推薦,將來的版本會廢棄。
componentWillMount() {
console.log('componentWillMount')
}
componentWillUpdate() {
console.log('componentWillUpdate')
}
componentWillReceiveProps() {
console.log('componentWillReceiveProps')
}
render() {
return (
<FragmentItem></FragmentItem>
)
}
}
export default class NewComponent extends React.Component {
state = {
propFlag: 2
}
// 使 OldLifecycleProvider 進入 componentWillReceiveProps 函數
componentDidMount() {
this.setState({
propFlag: 1
})
}
render() {
return (
<StrictMode> <OldLifecycleProvider propFlag={this.state.propFlag}></OldLifecycleProvider> </StrictMode>
)
}
}
複製代碼
渲染層級爲:NewComponent -> OldLifecycleProvider -> FragmentItem
,能夠看到在 React dev tool下依然能夠看到多層結構(Fragment並無顯示,比較遺憾,但願 dev tool 新版本能修復這個問題),但渲染出的 DOM 層級仍是扁平的,直接掛載在 div.view
下。
另外,因爲故意在 StrictMode 下使用了三個即將廢棄的API,打開 console ,可看到以下錯誤提醒:
Note: 項目可直接使用StrictMode,沒必要檢測是否爲開發環境,由於只在開發環境起做用。
若是很是注重項目代碼將來的可升級性,甚至能夠在最頂層用 StrictMode 包裹。但其實除此以外,若是項目穩定,開啓此模式對開發人員沒有一點好處,甚至還有額外的遷移工做,所以不建議在已開始項目使用;但對代碼重構有很是大的好處,可隨時提醒開發人員即將廢棄的 API 以便遷移。相信在 React 生態中會與 JS 的 'use strict'
同樣應用愈來愈普遍。
以前版本,若是想取得某個 Element 的 Ref,有兩種方式可選:
<input ref="input" />
=> this.refs.input
<input ref={input => (this.input = input)} />
=> this.input
其中字符串形式,因爲存在種種問題 (issue ,八卦下:這哥們就是 redux 做者)而不被推薦,具體內容就是:
this
取值,會使 React 稍稍變慢;this
與你想象的並不一致:import React from 'react'
class Children extends React.Component {
componentDidMount() {
// <h1></h1>
console.log('children ref', this.refs.titleRef)
}
render() {
return (
<div> {this.props.renderTitle()} </div>
)
}
}
export default class Parent extends React.Component {
// 放入子組件渲染
renderTitle = () => (
<h1 ref='titleRef'>{this.props.title}</h1>
)
componentDidMount() {
// undefined
console.log('parent ref:', this.refs.titleRef)
}
render() {
return (
<Children renderTitle={this.renderTitle}></Children>
)
}
}
複製代碼
由於字符串形式的 ref 綁定的 this 是根據渲染時而定,而不是聲明時而定,有點像 js 中函數的 做用域 和 this 的區別。但做爲 React 組件,咱們老是但願聲明時將 ref綁定在當前聲明的 Component 中,所以這也是個問題。
所以如今經常使用函數形式,幾乎沒有肯定,惟一的遺憾是須要新建函數;若是放入render裏,會影響性能;若是放在 class 下,又白白多了一個業務無關函數。可是如今咱們有了新的 API:createRef。基本用法:
class A extends React.Component {
inputRef = React.createRef()
componentDidMount() {
// 注意 current
this.inputRef.current.focus()
}
render() {
return (
<input type="text" ref={this.inputRef}></input>
)
}
}
複製代碼
經過 this.inputRef.current
便可獲取。this.inputRef
實際上是個原型爲 Object.prototype
的對象,並且目前爲止只有一個 current
鍵,對應的值是取得的 ref。看來 React 團隊已經預留好接口,接下來的版本會爲 Ref 增長新功能了。
相較於字符串形式,createRef
既在編碼中提早聲明須要獲取 Ref,又能夠避免字符串形式的種種硬傷;而像對於函數形式,能夠少寫一個函數,可是不夠靈活,實際編碼中可能仍是須要函數形式,這也是 React 文檔中將函數形式列爲高級技巧的緣由。所以做爲開發者,須要作到徹底避免字符串形式,儘可能使用createRef
,把函數形式列爲備選;而在 v16.3 版本中,看到 createRef
,無腦取 current
就好了。
以前是沒有ForwardRef
這種概念的,這是專門爲高階組件獲取 Ref 而設計。官方文檔(英文)Forwarding Refs 的例子摻雜了許多對高階組件(HOC)的介紹和理解,不夠純淨,不利於初步理解ForwardRef,原本挺簡單的一個概念被複雜化了,下面用簡單例子例子說明其基本用法:
import React from 'react'
// 高階組件,注意返回值用 `React.forwardRef` 包裹
// 裏面的無狀態組件接收第二個參數:ref
const paintRed = Component => React.forwardRef(
// 此例中,ref 爲 ForwardRef 中的 textRef
(props, ref) => (
<Component color='red' ref={ref} {...props}></Component>
)
)
class Text extends React.Component {
// 僅用於檢測是否取到 ref
value = 1
render() {
const style = {
color: this.props.color
}
return (
<p style={style}> 我是紅色的! </p>
)
}
}
const RedText = paintRed(Text)
export default class ForwardRef extends React.Component {
textRef = React.createRef()
componentDidMount() {
// value = 1
console.log(this.textRef.current.value)
}
render() {
// 若是沒有 forwardRef,那麼這個ref只能獲得 `RedText`,而不是裏面的 `Text`
return (
<RedText ref={this.textRef}></RedText>
)
}
}
複製代碼
今後例子看出,forwardRef 主要針對高階組件編寫者,用法流程以下:
forwardRef裏的參數只能是無狀態組件,那若是高階組件返回值不是個無狀態函數,是個有生命週期函數的 class 呢?React 官方文檔中已有這樣的例子,即在外面包一層無狀態組件,即:
const paintRed = Component => (() => {
// 新增 `componentDidMount`
class WhatEver extends React.Component {
static displayName = `PaintRed(${Component.displayName || Component.name || Unkown})`
componentDidMount() {
console.log('Mounted!')
}
render() {
// textRef 即爲最外層的 ref
const { textRef, ...props } = this.props
return (
<Component color='red' ref={textRef} {...props}></Component>
)
}
}
const forwardRef = React.forwardRef(
// 這裏再將 ref 的值做爲普通 props 傳遞便可
(props, ref) => (
<WhatEver textRef={ref} {...props}></WhatEver>
)
)
return forwardRef
})()
複製代碼
衆所周知,React 的 props 有兩個是私有的:key 和 ref,這二者是不能做爲普通props傳遞給子組件的。然而今後例子能夠看出,forwardRef 功能是:包裹的無狀態組件能夠接收 ref 做爲第二個參數,而且能夠傳遞下去。此時 ref 依然是 props 裏面私有的,仍是沒法從 props 取出,依然沒有打破原來的設計。
若是不用 createRef,而是用原來的兩種形式,都是正常的。
這個 API 給個人感受是用的不是不少,實際中必定要用高階組件的裏面的 ref 狀況很是少,並且大部分均可以經過 react 普通 api 解決,但總算是解決了一個原來的盲點,所以只能算是聊勝於無的新功能。但其實文檔中也提到了,大部分須要使用 forwardRef 的時候均可以用其餘方式解決。如在上面的源碼倉庫中,有個稍稍複雜的 forwardRef 的 demo,但其實仍是能夠不用 forwardRef 來實現相同功能,並且用的是新的生命週期函數實現,將在下次說新的生命週期鉤子時詳細講述。