React Hook简单快速上手

341 阅读8分钟

你是否还在为复用有状态的组件发愁,或是一直在使用复杂的render Props和HOC,现在就让Hook来解决你的所有烦恼

一、使用类组件或者函数组件的弊端

1.1 虽然React一直提倡的是我们能够函数组件,但是当我们发现函数组件中需要状态时,我们可能不得不将其改变成类组件

1.2 在使用类组件时,我们不得不去通过箭头函数或者bind去绑定,而这又是比较消耗性能

1.3 复杂的生命周期

1.4 在复用组件时,难免要使用render Props或者HOC,这会使代码结构看起来极其复杂,HOC会让组件之间更多的嵌套

二、为什么要使用Hook

2.1 能优化前面提到的问题

2.2 在复用组件时,能使用自定义Hook,复用组件更加简单

三、useState

3.1 先让我们来对比一下hook和使用传统类的区别

import React, {useState, memo, useMemo, useCallback} from 'react';

export function HookTest() {
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");

  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(age + 1)}>加一岁</button>
    <input type="text" onChange={(e) => setName(e.target.value)}/>
    </>
  )
}
import React, {Component} from 'react';

export class HookTest extends Component{
  constructor(props){
    super(props);
    this.state = {
      age: 18,
      name: "庄子鹏"
    }
  }

  setAge = () => {
    this.setState({
      age: this.state.age + 1
    })
  }

  setName = (value) => {
    this.setState({
      name: value
    })
  }

  render(){
    const {age, name} = this.state;
    return (
      <>
      <div>
        我叫{name}, 今天{age}岁
      </div>
      <button onClick={() => this.setAge()}>加一岁</button>
      <input type="text" onChange={(e) => this.setName(e.target.value)}/>
      </>
    )
  }
}

通过这里的例子看好像并没有什么很独特的区别,取了语法上的不同

3.2 每次渲染都是一次独立的闭包

  • 每一次渲染都有它自己的 Props 和 State
  • 每一次渲染都有它自己的事件处理函数
  • 当点击更新状态的时候,函数组件都会重新被调用,那么每次渲染都是独立的,取到的值不会受后面操作的影响

import React, {useState, memo, useMemo, useCallback} from 'react';
function HookTest(){
  let [age,setAge] = useState(0);
  function alertAge(){
    setTimeout(()=>{
      // alert 只能获取到点击按钮时的那个状态
      alert(age);
    },3000);
  }
  return (
      <>
          <p>{age}</p>
          <button onClick={()=>setAge(age+1)}>+</button>
          <button onClick={alertAge}>alertAge</button>
      </>
  )
}

  
  //这里我们用传统类写法来实现以下
import React, {Component} from 'react';

export default class HookTest extends Component {
  constructor(props) {
    super(props);
    this.state = {
      age: 0
    }
  }

  setAge = () => {
    this.setState({
      age: this.state.age + 1
    })
  }

  alertAge = () => {
    setTimeout(() => {
      alert(this.state.age)
      // alert 能获取到最新的那个state age    });
    }, 3000)
  }

  render() {
    const {age} = this.state;
    return (
      <>
      <p>{age}</p>
      <button onClick={() => this.setAge()}>+</button>
      <button onClick={() => this.alertAge()}>alertAge</button>
      </>
    )
  }
}

这里我们也使用了传统的类写法去写,确实能够获得最新的状态

3.3 函数式更新

如果我们需要去通过上一次的最新的state去计算下一个state的值,可以将回调函数作为参数传给setState,该回调函数将接收先前的 state,并返回一个更新后的值。

import {useState, useEffect} from 'react';

export default function Counter2() {
  let [age, setAge] = useState(0);

  function lazyAdd() {
    setTimeout(() => {
      // alert 只能获取到点击按钮时的那个状态
      setAge(age => age + 1);
    }, 3000);
  }

  return (
    <>
    <p>{age}</p>
    <button onClick={() => setAge(age + 1)}>+</button>
    <button onClick={lazyAdd}>lazyAdd</button>
    </>
  )
}

//在这里我其实更想和上面的alert那个例子做比较,但是我发现好像没办法实现去alert最新的那个状态

3.4 这里我们还要讲一下Hook触发重新渲染的条件,Hook使用的是Object.is

import React, {useState, memo, useMemo, useCallback} from 'react';

export function HookTest() {
  console.log('render');
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");

  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(18)}>加一岁</button>
    //点击时并不会触发重新渲染,因为前后状态相同
    <input type="text" onChange={(e) => setName(e.target.value)}/>
    </>
  )
}

import React, {Component} from 'react';

export class HookTest extends Component{
  constructor(props){
    super(props);
    this.state = {
      age: 18,
      name: "庄子鹏"
    }
  }

  setAge = () => {
    this.setState({
      age: 18
    })
  }

  setName = (value) => {
    this.setState({
      name: value
    })
  }

  render(){
    console.log('render');
    const {age, name} = this.state;
    return (
      <>
      <div>
        我叫{name}, 今天{age}岁
      </div>
      <button onClick={() => this.setAge()}>加一岁</button>
      //点击时会触发重新渲染,即使前后状态相同
      <input type="text" onChange={(e) => this.setName(e.target.value)}/>
      </>
    )
  }
}

五、useEffect

5.1 useEffect是Hook中带来的一个新概念,叫做副作用,取代了传统class中的生命周期函数,Hook没有了所谓的生命周期函数

在这里我不是很理解副作用的含义,但我们可以把他理解成是组件渲染的一部分

import React, {useState, memo, useMemo, useCallback, useEffect} from 'react';

export function HookTest() {
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");

  useEffect(() => {
    document.title = name;
  })

  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(age + 1)}>加一岁</button>
    <input type="text" onChange={(e) => setName(e.target.value)}/>
    </>
  )
}
// 每次我setName之后,title都会改变

那每次触发重新渲染之后都会去执行useEffect里的内容,那如何去避免执行useEffect中的内容,这时候我们需要用到useEffect的第二个参数,依赖项

import React, {useState, memo, useMemo, useCallback, useEffect} from 'react';

export function HookTest() {
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");

  useEffect(() => {
    document.title = age + name;
  },[name])

  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(age + 1)}>加一岁</button>
    <input type="text" onChange={(e) => setName(e.target.value)}/>
    </>
  )
}
//只有当状态name发生改变且触发重新渲染时,useEffect里面的内容才会去执行

5.2 使用多个Effect

我们会在传统的react中使用多个生命周期,同样我们可以使用多个Effect来实现同样的功能,Effect的执行按照顺序来进行

import React, {useState, memo, useMemo, useCallback, useEffect} from 'react';

export function HookTest() {
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");

  useEffect(() => {
    console.log(1)
  })

useEffect(() => {
  console.log(2)
})  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(age + 1)}>加一岁</button>
    <input type="text" onChange={(e) => setName(e.target.value)}/>
    </>
  )
}

六、useReducer

6.1 在管理多个状态时可以使用useReducer

import React, {useReducer} from 'react';

const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {age: state.age + 1};
    case 'decrement':
      return {age: state.age - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
  return {age:initialState};
}
export function HookTest(){
  const [state, dispatch] = useReducer(reducer, initialState,init);
  return (
    <>
    Count: {state.age}
    <button onClick={() => dispatch({type: 'increment'})}>+</button>
    <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  )
}

七、useContext

首先我们先来看一下传统react的context使用

//首先会有这么一个场景
class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}//当我们使用context
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}// 从官方的实例来看,并没有差多少
import React, {useContext} from 'react';

const ThemeContext = React.createContext('light');

export class HookTest extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar/>
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton/>
    </div>
  );
}

function ThemedButton() {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  const theme = useContext(ThemeContext);
  return <button>{theme}</button>;
}

八、useRef

还是一样,我们先看一下传统react使用ref和使用Hook的区别

import React, {Component} from 'react';

export class HookTest extends Component{
  constructor(props){
    super(props);
    this.state = {
      age: 18,
      name: "庄子鹏"
    }
    this.nameInput = null;
  }

  setAge = () => {
    this.setState({
      age: this.state.age + 1
    })
  }

  setName = (value) => {
    this.setState({
      name: value
    })
    console.log(this.nameInput.value);

    //可以获取dom
  }

  render(){
    const {age, name} = this.state;
    return (
      <>
      <div>
        我叫{name}, 今天{age}岁
      </div>
      <button onClick={() => this.setAge()}>加一岁</button>
      <input 
        type="text" 
        ref={(ele) => this.nameInput = ele}   //使用ref回调的方式
        onChange={(e) => this.setName(e.target.value)}
      />
      </>
    )
  }
}

import React, {useState, useRef} from 'react';


export function HookTest() {
  let [age, setAge] = useState(18);
  let [name, setName] = useState("庄某某");
  const nameInput = useRef(null);

  function go(value){
    setName(value);
    console.log(nameInput.current.value);  
    //获取的dom实例都会被包装成一个对象,  {current: dom}
  }

  return (
    <>
    <div>
      我叫{name}, 今天{age}岁
    </div>
    <button onClick={() => setAge(age + 1)}>加一岁</button>
    <input
      type="text"
      ref={nameInput}
      onChange={(e) => go(e.target.value)}
    />
    </>
  )
}

九、自定义Hook

9.1 自定义hook可以理解为就是使用hook自己定义的一些组件

9.2 自定义hook必须使用use开头,这是一种规则

import React, {useState, useRef, useEffect} from 'react';

function useAge() {  //命名必须使用use开头
  let [age, setAge] = useState(18);
  useEffect(() => {
    setInterval(() => {
      setAge(age => age + 1);
    }, 1000);
  }, []);
  return [age, setAge];
}

// 每个组件调用同一个 hook,只是复用 hook 的状态逻辑,并不会共用一个状态
function Counter1() {
  let [age, setAge] = useAge();
  return (
    <div>
      <button onClick={() => {
        setAge(age + 1)
      }}>{age}</button>
    </div>
  )
}

function Counter2() {
  let [age, setAge] = useAge();
  return (
    <div>
      <button onClick={() => {
        setAge(age + 1)
      }}>{age}</button>
    </div>
  )
}

export function HookTest(){
  return (
    <>
      <Counter1/>
      <Counter2/>
    </>
  )
}