React Hooks 指北(四):useReducer

1,022 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

1.useReducer介绍

官网定义:useReducer是useState的替代方案,当数据处理较为复杂时,可以选择使用useReducer。许多小伙伴第一眼看到useReducer以为是Redux的方案,其实它和redux毛关系都没有。useReducer仅仅是数据响应式,不具备组件数据共享的特点。

2.使用

const [num,dispatch] = useReducer(fn,number)

useReducer的使用可以对比useState。其左边是一个数组,其中数组的第一个数据接收初始的数据,数组的第二个是一个函数,专门用来修改初始数据。这个特点和useState一模一样。 接下来是useReducer的右边。右边传入的第一个参数是自定义修改数据的函数,第二个参数是初始数据

import { useReducer } from "react"
//1.初始数据
let number = 1
//2.修改数据的函数
function reducer(state,action) {
  if(action==='add') {
    return state+1
  } else if(action==='del') {
    return state-1
  }
  return state
}

export default function Son(props) {

  const [num,dispatch] = useReducer(reducer,number)

  return <div>
    <div>num:{num}</div>
    <div onClick={()=>dispatch('add')}>+1</div>
    <div onClick={()=>dispatch('del')}>-1</div>
  </div>
}

效果

2.gif

3.useReducer的复用性

当我们有多个组件或者多处需要同一份数据处理逻辑时,可以使用一份reducer实现。记住仅仅是功能上复用,不存在数据共享的特点。如下例子

App.js

app组件引入了组件son1和son2。其中son1组件和son2组件引入同一份数据和修改数据的方法

import Son1 from "./views/Son"
import Son2 from "./views/Son1"
function App() {
  return <div>
    <Son1/>
    <div>------------------</div>
    <Son2/>
  </div>
}
export default App;

common.js

保存数据和修改数据的逻辑

export let number = 1
export function reducer(state,action) {
  if(action==='add') {
    return state+1
  } else if(action==='del') {
    return state-1
  }
  return state

Son1组件

import { useReducer } from "react"
import {number,reducer} from "./common"

export default function Son1(props) {

  const [num,dispatch] = useReducer(reducer,number)
  return <div>
    <div>num:{num}</div>
    <div onClick={()=>dispatch('add')}>+1</div>
    <div onClick={()=>dispatch('del')}>-1</div>
  </div>
}

Son2组件

import { useReducer } from "react"
import {number,reducer} from "./common"
export default function Son2(props) {

  const [num,dispatch] = useReducer(reducer,number)
  return <div>
    <div>num:{num}</div>
    <div onClick={()=>dispatch('add')}>+1</div>
    <div onClick={()=>dispatch('del')}>-1</div>
  </div>
}

1.gif

通过上述代码发现,son1组件和son2组件使用相同的数据,并且使用相同的reducer处理函数。 观察页面显示,二者页面数据修改并不会影响其它页面数据。因此useReducer不具备数据共享功能。只是useState的替代方案。

4.考虑如何模拟组件数据共享(类似redux)

方案一:使用useState+父子数据通信

在根组件上定义useState定义初始数据,然后使用父组件给儿子后代传递一个函数,这个函数可以用来通知父组件修改数据。

App.js

import Son1 from "./views/Son"
import Son2 from "./views/Son1"
import {useState} from "react"
function App() {
  //全局数据
  const [num,setNum] = useState(1)
  //全局修改数据
  function changeFn(number) {
    setNum(num+number)
  }
  return <div className="app">
    <Son1 num={num} change = {changeFn} />
    <div>-------------------</div>
    <Son2 num={num} change = {changeFn}/>
  </div>
}
export default App;

Son1.js

export default function Son1(props) {  
  return <div>
    <div>num:{props.num}</div>
    <div onClick={()=>props.change(1)}>+1</div>
    <div onClick={()=>props.change(-1)}>-1</div>
  </div>
}

Son2.js

export default function Son2(props) {  
  return <div>
    <div>num:{props.num}</div>
    <div onClick={()=>props.change(1)}>+1</div>
    <div onClick={()=>props.change(-1)}>-1</div>
  </div>
}

效果

3.gif

我们可以看到此时的组件son1和son2是可以共享全局数据的,任意一处数据的修改,其它组件也会同步修改数据。但是自己考虑,如果组件的层级增加到3层或者4层?那么每个组件都要向下传递函数,此时维护也变得复杂。

方案二:useState+useContext

组件结构图

可以看到根组件包含两个儿子组件son1和son2.而son1组件又包含一个子组件son3

image.png

方案思路

根组件app.js使用useState添加全局数据,并且定义修改state的函数。使用useContext可以传递数据的特性,将state数据和修改state的函数传递给所有的后代组件。后代组件可以获取state数据,也可以接收到修改state的函数。

App.js

import Son1 from "./views/Son"
import Son2 from "./views/Son1"
import {useState,createContext} from "react"

export const Context = createContext()
function App() {
  //1.全局数据
  const [num,setNum] = useState(1)
  //2.全局修改数据的函数
  function changeFn(number) {
    setNum(num+number)
  }
  return <div className="app">
   //3.使用useContext将数据和函数传递给后代
    <Context.Provider value={{num:num,changeFn:changeFn}}>
      <Son1/>
      <div>-------------------</div>
      <Son2 />
    </Context.Provider>
  </div>
}
export default App;

Son1.js

import {Context} from "./../App.js"
import { useContext } from "react"
import Son3 from "./Son3.js"
export default function Son1() {  
  const context = useContext(Context)
  return <div>
    <div>num1:{context.num}</div>
    <div onClick={()=>context.changeFn(1)}>+1</div>
    <div onClick={()=>context.changeFn(-1)}>-1</div>
    <div>-----</div>
    <Son3/>
  </div>
}

Son2.js

import {Context} from "./../App.js"
import { useContext } from "react"
export default function Son2() {  
  const context = useContext(Context)
  return <div>
    <div>num2:{context.num}</div>
    <div onClick={()=>context.changeFn(1)}>+1</div>
    <div onClick={()=>context.changeFn(-1)}>-1</div>
  </div>
}

Son3.js

import {Context} from "./../App.js"
import { useContext } from "react"
export default function Son3() {  
  const context = useContext(Context)
  return <div>
    <div>num3:{context.num}</div>
    <div onClick={()=>context.changeFn(1)}>+1</div>
    <div onClick={()=>context.changeFn(-1)}>-1</div>
  </div>
}

效果图

可以发现,任意一处组件值的修改,都可以做到全局修改,成功实现了组件数据共享和传递。

4.gif

方案三 useReducer+useContext

既然useState+useContext可以完成数据共享,那作为useState的替代品,我们当然可以使用useContext+useReducer实现相同的数据共享。

组件结构图

image.png

设计方案

根组件App使用useReducer创建全局数据和修改全局数据的函数,使用useContext将数据和修改的函数传递给后代组件。后代组件接收并使用。

App.js

import Son1 from "./views/Son"
import Son2 from "./views/Son1"
import "./App.css"
import {createContext,useReducer} from "react"
//1.全局数据
let num = 1
//2.全局修改数据
function reducer(state,action) {
  if(action==='add') {
    return state+1
  } else if(action==='del') {
    return state-1
  }
  return state
}
//3.定义context传递数据和函数给后代
export const Context = createContext()
function App() {
  const [number,dispatch] = useReducer(reducer,num)

  return <div className="app">
    <Context.Provider value={{number:number,dispatch:dispatch}}>
      <Son1/>
      <div>-------------------</div>
      <Son2 />
    </Context.Provider>
  </div>
}
export default App;

Son1.js

import {Context} from "./../App.js"
import { useContext } from "react"
import Son3 from "./Son3.js"
export default function Son1() {  
  const context = useContext(Context)
  return <div>
    <div>son1:{context.number}</div>
    <div onClick={()=>context.dispatch('add')}>+1</div>
    <div onClick={()=>context.dispatch('del')}>-1</div>
    <div>------</div>
    <Son3/>
  </div>
}

Son2.js

import {Context} from "./../App.js"
import { useContext } from "react"
export default function Son2() {  
  const context = useContext(Context)
  return <div>
    <div>son2:{context.number}</div>
    <div onClick={()=>context.dispatch('add')}>+1</div>
    <div onClick={()=>context.dispatch('del')}>-1</div>
  </div>
}

Son3.js

import {Context} from "./../App.js"
import { useContext } from "react"
export default function Son3() {  
  const context = useContext(Context)
  return <div>
    <div>son3:{context.number}</div>
    <div onClick={()=>context.dispatch('add')}>+1</div>
    <div onClick={()=>context.dispatch('del')}>-1</div>
  </div>
}

效果图

4.gif

总结

  • useReducer是useState的替代品,二者都是为了完成数据响应式
  • 当数据是简单数据,如Number,boolean.string或者简单对象时,优先用useState完成数据响应式 而数据是复杂对象,并且数据修改较为繁杂时考虑使用useReducer实现数据响应式
  • 当数据的修改存在多个组件复用时,可以考虑使用useReducer完成数据抽离和响应式
  • useState+useContext或者useReducer+useContext可以轻松实现类似Reducer的全局数据共享