在react native中使用hooks

5,935 阅读5分钟

Facebook 于本月 12 号发布了 React Native v0.59,支持了hooks 的使用。让我们一起看下如何使用吧

什么是hooks ?

官方原话

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class

说白了就是在react 函数组件中,也可以使用类组件(classes components)的 state 和 组件生命周期。

why hooks ?

复用问题

复用一个有状态的组件太难了,但是还是能复用的。官方怎么做的呢?renderPropshoc,但是你会发现你的组件层级又多,嵌套又深,出问题的时候定位非常麻烦。

逻辑复用问题,这方面大家最熟悉的相关库recompose, 但是作者本人已经加入react 团队了而且还发了声明

Hi! I created Recompose about three years ago. About a year after that, I joined the React team. Today, we announced a proposal for Hooks. Hooks solves all the problems I attempted to address with Recompose three years ago, and more on top of that. I will be discontinuing active maintenance of this package (excluding perhaps bugfixes or patches for compatibility with future React releases), and recommending that people use Hooks instead. Your existing code with Recompose will still work, just don't expect any new features. Thank you so, so much to @wuct and @istarkov for their heroic work maintaining Recompose over the last few years.

翻译一下就是Hooks解决了我三年前尝试用Recompose解决的所有问题,并且更多地解决了这个问题,并且停止维护这个库。

生命周期

跟 vue 相比,react的各种生命周期实在是太多了。但是 hooks就是一个生命周期 useEffect,它是一个 ,你可以把它当成componentDidMount, componentDidUpdate, and componentWillUnmount 的集合。具体我会在下面篇幅介绍。

this的问题

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
    	count: 0,
    	count1: 0, 
    	count2: 0
    },
    this.setCount = this.setCount.bind(this);
  
  }
  setCount () {}
  setcout = () => {}
  
  render() {
   <button onClick={this.setCount}>测试0</button>
   <button onClick={this.setCount}>测试1</button>
  }
}

就算用 bind 还是箭头函数还是要小心this 的问题。但是在fp 编程方式里面是没有 this 概念的

准备工作

react >=16.8 react native >= 0.59

常用 api

  • useState
  • useEffect
  • useReducer
  • useRef
  • useContext

下面逐一来介绍

const App = () => {
  const [loading, updateLoading] = useState(true)
  const [list, updateList] = useState([])
  const [title, updateTitle] = useState(undefined)
  useEffect(() => {
    getData()
  }, [])
  const getData = () => {
    fetch('https://api.douban.com/v2/movie/in_theaters?city=广州&start=0&count=10')
    .then(response => response.json()
    )
    .then(res => {
      updateTitle(res.title)
      updateList(res.subjects)
      updateLoading(false)
    })
  }
  return (
    <View style={styles.container}>
      <Text style={styles.welcome}>{title}</Text>
      {loading ? <Text>加载中---</Text> : list.map(i => <Text key={i.id} style={styles.instructions}>{i.title}</Text>)}
    </View>
  )
}

useState

 const [loading, updateLoading] = useState(true)

先定义好state,数组第一个值是你需要更新的值,第二个值是更新该值得函数,useState() 函数传入的初始值。更新的时候调用 updateLoading(true)就行了

useEffect

通俗点就是 componentDidMount,componentDidUpdate、componentWillUnmount三个生命周期的合集。渲染后必然会触发,怎么理解呢?就是你通过 updateLoading 函数去更新 loading 的值,页面重新 render ,这个时候这个方法就会被触发。

问题来了我上面的代码岂不是会重复请求直到 💥💥💥 ?
useEffect 可以传入第二个参数来避免性能的损耗,如果第二个参数数组中的成员变量没有变化则会跳过此次改变。传入一个空数组 ,那么该 effect 只会在组件 mount 和 unmount 时期执行,一般来说传个唯一的 id 是最佳作法。

如何做一些取消操作呢?

比如定时器,发布订阅,有时候组件卸载的时候要取消这个时候该怎么办呢 ? 可以return 一个新的函数

注意一个坑点

当我准备用定时器去模拟请求的时候发现一个问题?

function Counter() {
  let [count, setCount] = useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <Tex>{count}</Text>;
}

我发现我的数字是 1 不会动了!! why ?

问题在于,useEffect 在第一次渲染时获取值为 0 的 count,我们不再重执行 effect,所以 setInterval 一直引用第一次渲染时的闭包 count 0,以至于 count + 1 一直是 1。经过不断的实现,发现可以用新的 api 来规避问题。一种是通过 useRef, 一种是用过 useReducer

useReducer

useState的替代方案。接受类型为(state,action) => newState的reducer,并返回与dispatch方法配对的当前状态。 (如果熟悉Redux,你已经知道它是如何工作的。) 用过 redux的相信对这个reducer 都不陌生 使用和redux 如出一撤。
🌰 actions.js

export const loading = 'LOADING'
export const list = 'LIST'
export const updateLoading = (data) => ({
  type: loading,
  data
})
export const updateList = (data) => ({
  type: list,
  data
})
    

🌰 reducer.js

import {
  loading,
  list,
} from './actions'
export const initState = {
  loading: true,
  list: [],
}
export const initReducer = (state, {type, data}) => {
  switch (type) {
    case list:
      return {
        ...state,
        list: data
      }
    case loading:
      return {
        ...state,
        loading: data
      }
    default:
      return state
  }
}

最后连接组件

import React, { useReducer, useEffect, useRef } from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import { updateLoading, updateList } from './store/actions'
import { initState, initReducer } from './store/reducers'

const App = () => {
  const [state, dispatch] = useReducer(initReducer, initState)
  useEffect(() => {
    getData()
  }, [])
  const getData = () => {
    fetch('https://api.douban.com/v2/movie/in_theaters?city=广州&start=0&count=10')
    .then(response => response.json()
    )
    .then(res => {
      dispatch(updateList(res.subjects))
      dispatch(updateLoading(false))
    })
  }
  const {list = [], loading} = state 
  return (
    <View style={styles.container}>
      {loading ? <Text>加载中---</Text> : list.map(i => <Text key={i.id} style={styles.instructions}>{i.title}</Text>)}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});
export default App

效果图

useRef

这个没什么主要多了 current 这一层

import React, { useReducer, useEffect, useRef } from 'react';
import {Platform, StyleSheet, Text, View, TextInput } from 'react-native';
import { updateLoading, updateList } from './store/actions'
import { initState, initReducer } from './store/reducers'

const App = () => {
  const _ref = useRef()
  const [state, dispatch] = useReducer(initReducer, initState)
  useEffect(() => {
    getData()
  }, [])
  const getData = () => {
    fetch('https://api.douban.com/v2/movie/in_theaters?city=广州&start=0&count=10')
    .then(response => response.json()
    )
    .then(res => {
      dispatch(updateList(res.subjects))
      dispatch(updateLoading(false))
    })
  }
  const {list = [], loading} = state 
  return (
    <View style={styles.container}>
      {loading ? <Text>加载中---</Text> : list.map(i => <Text key={i.id} style={styles.instructions}>{i.title}</Text>)}
      <TextInput ref={_ref} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});
export default App

自定义hooks

文章开头说过复用问题,hooks 怎么做的更好其实就是可以自定义hooks。比如我现在要封装一个 请求hooks 可以这么干

import React, { useState, useEffect  } from 'react';
export const request = (initData) => {
  const [data, updateData] = useState(initData)
  const [url, updateUrl] = useState(null)
  const [isLoading, updateLoading] = useState(true)
  const [isError, updateError] = useState(false)
  useEffect(() => {
    fetchData()
  }, [url])
  const fetchData = () => {
    if(!url) return 
    updateLoading(true)
    try {
      fetch(url).then(res => res.json()).then(res => {
        updateData(res)
      })
    } catch (error) {
      updateError(error)
      console.log(error)
    }
    updateLoading(false)
  }
  const doFetch = url => {
    console.log(url, 'url')
    updateUrl(url)
  }
  return { data, isLoading, isError, doFetch }
}

如何调用呢

  const { data, isLoading, doFetch } = request([])
  useEffect(() => {
    getData()
  }, [])
  const getData = async () => {
    const url = 'https://api.douban.com/v2/movie/in_theaters?city=广州&start=0&count=10'
    doFetch(url)
  }

结语

最开始出来测试版的时候,还在观望。现在在 pc 上用了一段时间后,感觉真香,越来越喜欢hooks 。