「ReactNaitve」對hooks最佳實踐的探索

weixin_34148340發表於2019-01-14

author: 軒邈

一、hooks介紹

一. useState

  1. eg: const [count, setCount] = useState(0)

  2. 介紹

    (1)有一個引數:預設值, 可以是函式,只在初始渲染時執行一次

    (2)返回一個帶有兩個元素的陣列

    (3)第一個元素是 state 的值, 第二個元素是更新state的函式,每次新的,使用useCallback,可以讓它使用上次的函式;函式名隨意不一定是set某某某,引數可以是要更新的值或者函式,當是函式時,函式引數是上一次state的值。

 (4) 如果有多個state則根據 useState 的呼叫順序來“記住”每個state的狀態歸屬 
 
 (5)這個特性要求Hooks不可以寫在if或者switch等可能不執行的程式碼片段,會導致呼叫次序不一致
複製程式碼

二. useLayoutEffect

簽名與 useEffect 相同,在DOM變化(layout佈局)後同步觸發,渲染之(paint繪製)執行 ,適用於使用者可見的 DOM 改變。

三. useEffect

componentDidMountcomponentDidUpdate 不同,傳遞給 useEffect 的函式在DOM變化(layout佈局) 和渲染(paint繪製)觸發。 這使得它適用於許多常見的 side effects ,例如設定訂閱和事件處理程式,因為大多數型別的工作不應阻止瀏覽器更新(判斷標準)螢幕。

1. eg:
  `componentDidMount:  useLayoutEffect(() => {setTitle(1)}, [])`   

  `componentDidUpdate:  useLayoutEffect(() => {console.log(1)})` 

  `componentWillUnmount: useLayoutEffect()=>{return () => {console.log('我要解除安裝元件啦')}}` 

  我是一個方法,元件更新後返回,下次元件更新前執行:`useLayoutEffect(() => {return () => {console.log(‘我是一個方法,元件更新後返回,下次元件更新前執行')}}, [count, count2])` 
複製程式碼
2. 介紹:相當於 componentDidMount 、 componentDidUpdate 、componentWillUnmount和 我是一個方法元件更新後返回,下次元件更新前執行
3. 有兩個引數
  1. 第一個為函式,預設會在渲染完成後執行一次,如果返回的是一個函式,則返回的函式會在第二個引數陣列裡面的元素髮生變化且渲染完成後執行 
  2. 第二個為一個陣列(也可以寫成常量等型別,不過不會呼叫引數一),裡面寫需要監控的state,當state改變時會呼叫第一個函式,不改變則不會呼叫引數一,當陣列為空時,只會在最開始呼叫一次,相當於componentDidMount;不傳時,預設監控所有state,相當於componentDidUpdate
複製程式碼

四. useContext

1. eg:
  const theme = useContext(ThemeContext) 
複製程式碼
2. 使用 Contex時
  ```javascript
  const ThemeContext = React.createContext();
  const LanguageContext = React.createContext();
  ```

  ```js
  <ThemeContext.Consumer>
      {
          theme => (
              <LanguageContext.Cosumer>
                  language => {
                      //可以使用theme和lanugage了
                  }
              </LanguageContext.Cosumer>
          )
      }
  </ThemeContext.Consumer>
  ```

  兩個render props寫法,兩個巢狀看起來麻煩很多


  使用useContext時 

  ```js
  const theme = useContext(ThemeContext);
  const language = useContext(LanguageContext);
  // 這裡就可以用theme和language了
  ```

  接受一個由React.createContext返回的上下文物件,寫法簡化很多並且不再需要理解render props 
複製程式碼
3. 有時候會造成意想不到的重新渲染
  ```javascript
  const ThemedPage = () => {
      const theme = useContext(ThemeContext);
      return (
         <div>
              <Header color={theme.color} />
              <Content color={theme.color}/>
              <Footer color={theme.color}/>
         </div>
      );
  };
  ```

  當theme的其他屬性(如size等其他非color屬性)改變時也會導致介面重新渲染 
複製程式碼

五. useReducer

1. 介紹:增強版的useState, 小型Redux (機制類似,但是不同元件內部資料認識獨立的,)
2. eg:
  ```javascript
  const initialState = { count: 0 } 
  const reducer = function reducer(state, action) { 
    switch (action.type) { 
      case 'reset': 
      	return initialState 
      case 'increment': 
      	return { ...state, count: state.count + 1 } 
      case 'decrement': 
      	return { ...state, count: state.count - 1 } 
      default: 
      	return state 
    }
  }
  ```

  //上面這部分應該寫在函式元件外面防止函式一遍遍的建立

  const [count3, count3Dispatch] = useReducer(reducer, initialState) 
複製程式碼
3. 和useState相比dispatch和reducer只被建立了一次

六. useCallback

1. 介紹:返回值為 memoized 回撥函式,第一個引數為一個函式,第二個為一個陣列,只有內部元素變化,返回的回撥函式才會被重新建立
2. eg:
  ```javascript
  const [count4, setCount4] = useState(0) 
  const counterRef = useRef(count4) 
  
  useEffect(
    () => { 
      counterRef.current = count4 
    }, 
    [count4] 
  )
  
  const incrementCount4 = useCallback(() => setCount4(counterRef.current + 1), []) 
  ```
複製程式碼
  1. 儘量少用,一般都能用useReducer優化

七. useMemo

介紹:useCallback(fn, inputs) 等價於 useMemo(() => fn, inputs)

八. useRef

介紹:useRef 返回一個可變的 ref 物件,其 .current 屬性被初始化為傳遞的引數(initialValue)。返回的物件將存留在整個元件的生命週期中。

九. useImperativeMethods

  1. useImperativeMethods 自定義使用 ref 時公開給父元件的例項值,方便對函式式元件進行ref操作 使呼叫內部函式成為可能
  2. 應儘量避免這種程式碼

十. 自定義Hooks興起可形成一種約定,程式碼之間的公用邏輯可以使用useXXX形式的函式

```javascript
// eg:  
const useMountLog = name => {
    useEffect(() => {
        console.log(`${this.name}元件渲染時間->${this.end - this.begin}ms`)
    },[])
}
```
複製程式碼
// eg: usePrevious 
function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}
複製程式碼

二、環境構建

  1. git clone github.com/facebook/re… (我使用時react版本為16.6.1)
  2. cd react
  3. packages/shared目錄下所有檔案中的enableHooks = false替換為enableHooks = true
  4. 執行yarn install
  5. 執行yarn build -- --type=RN_OSS
  6. 等執行完畢後,將 build/react-native/ 下的內容替換 專案路徑/node_modules/react-native/Libraries/Renderer(我使用時react-native版本為0.57.8)下的內容
  7. 專案中react版本為16.7.0-alpha.2(16.7.0-alpha.0~16.7.0-alpha.2皆可,16.7.0正式版將hooks移除了)

三、原有專案改造

  1. 替換類元件

    // 改造前
    export default class HomeScene extends Component{...}
    // 改造後
    export default forwardRef((props, ref) => {...}) // forwardRef包起來是方便函式元件內部方法呼叫
    複製程式碼
  2. 建構函式移除

  3. state狀態修改和建立改為useState const [visible, setVisible] = useState(false)

  4. 改造前父元件通過ref引用子元件的方法,改造後使用forwardRef將createRef建立的ref傳遞到子元件內部,再使用useImperativeMethods將內部方法繫結到傳進來的ref上

    useImperativeMethods(ref, () => ({ showModal: this.showModal }), [])
    複製程式碼
  5. 方法

    // 改造前
    showModal = (from, data, ticket) => {...}
    // 改造後
    this.showModal = (from, data, ticket) => {...} // 推薦使用箭頭函式方便函式間呼叫
    複製程式碼
  6. render改為return,按state狀態變化將原先render前的邏輯移入對應useEffect。

  7. 引入redux-react-hook

    (1)引入StoreContext將根元件包起來

    <StoreContext.Provider value={store}>
    	...
    </StoreContext.Provider>
    複製程式碼

    (2)```javascript // 改造前 const { name, cityName } = this.props.UserInfo // 改造後 const mapState = React.useCallback(state => state.UserInfo, []) const { name, cityName } = useMappedState(mapState)

    
    (3)每次呼叫useMappedState都會執行subscribe store。 但是,如果store更新,你的元件將只重新渲染一次。 因此,多次呼叫useMappedState(例如封裝在自定義hooks中)不應該對效能產生很大影響。 如果測試發現效能影響較大,可以嘗試返回物件。
    
    複製程式碼
  8. 引入react-navigation-hooks,需升級react-navigation至最新版(3.1.0)適配。

(1)使用

  ```javaScript
  const { navigate } = useNavigation()
  ```

  與原先的路由管理共用一套路由
複製程式碼

(2)注意:

  1) 如果專案是用pod管理,該RNGestureHandler.podspec裡面路徑有問題需要修改。
  
  2) createBottomTabNavigator的第二個引數BottomTabNavigatorConfig的navigationOptions屬性改為了defaultNavigationOptions
複製程式碼

四、注意事項

  1. 原先的元件增強基礎設施,如高階元件和反向整合之類的元件需要進行相應修改以適應新的函式元件。
  2. forwarfRef需要額外關注,因為forwardRef包裹元件後返回的是React節點需要單獨處理target.$$typeof === Symbol.for('react.forward_ref')(我暫時是這樣處理的)
  3. 需要注意增強元件的name或displayName設定,方便錯誤定位
  4. eslint-plugin-react-hooks: (1)在每個渲染上以相同的順序呼叫 Hooks (不能在迴圈、判斷、switch中使用)。 (2)對 Hooks 的呼叫要麼在 PascalCase 函式內(假設是一個元件),要麼是另一個 useSomething 函式(假定為自定義Hook)。
  5. 私以為hooks最大的亮點還是在於邏輯複用的易用性提升上,這個需要我們及時轉變思路,習慣函式式元件開發。
  6. 如果inputs為[](useCallback的第二個引數),React每次渲染都將使用同樣的函式返回值(不管裡面的運算),這時你可以選擇在函式元件外宣告這個函式,但是React官方並不推薦這樣做,hooks的重點就是是允許你保留元件中的所有內容。
  7. 我們對元件增強時,元件的回撥一般不需要銷燬監聽,而且僅需監聽一次,這與 DOM 監聽不同,因此大部分場景,我們需要利用 useCallback 包裹,並傳一個空陣列,來保證永遠只監聽一次,而且不需要在元件銷燬時登出這個 callback。
  8. hooks目前還有很多坑,是趨勢,可瞭解,慎用。

原文連結: tech.meicai.cn/detail/81, 也可微信搜尋小程式「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。

相關文章