这个 hook api,曾吓退许多前端开发者

简介: 这个 hook api,曾吓退许多前端开发者

在 React 的学习过程中,有一个大 boss 拦路虎。他不仅概念多,理解起来困难,使用起来也很麻烦,他给 React 学习者带来了巨大的痛苦。因此他臭名昭著。有许多前端开发者因为讨厌他而放弃了 React。但怪就怪在,很多大佬会觉得这个方案非常厉害。


他就是 redux.


在刚开始的时候,redux 几乎是 React 项目中的唯一状态管理方案,为了解决他的一系列问题,基于 redux 又发展出来许多技术方案,例如 redux-thunk,redux-saga,dva 等,这又无形中增加了大量的学习成本。


正是由于他臭名昭著,以致于在 react hooks 出来之后,大家都在积极探索如何在项目中寻找替代 redux 的状态管理方案。最后他才开始逐渐淡化。许多项目开始放弃使用 redux,寻找其他的替代品,例如,基于数据劫持的 Mobx,使用更简单的 zustand,官方团队推出的 Recoil,以及我自己封装 Moz


Moz 对 TS 的支持非常完善,能自动推导出返回类型,无需额外定义,小型轻量,学习成本低,欢迎大家给我点个 star

https://github.com/yangbo5207/moz


但是,如果想要成为一名资深的 React 使用者,redux 始终是我们绕不开的点。react hooks 的底层实现也大量借鉴了 redux 的思路,可能你在使用层面看到的是 useState,但是底层实现里还是 redux,react hooks 也提供了一个与 redux 概念几乎一样的 hook


useReducer


如果你不去封装一些底层库,可能会很少在项目中使用到他,因此有的人在学习过程中会忽视他的重要性。但是他的思想在大型项目中非常有用。我们借助一个场景来逐渐了解他。


场景


在许多的编辑器项目中,例如富文本编辑器,MD 编辑器,思维导图编辑器,低代码平台编辑器,代码编辑器...


我们会遇到一个非常常规的需求:撤销:向后撤销、向前撤销,ctrl + z shift + ctrl + z。作为使用者,相信大家都非常熟悉。但是作为开发者,要如何基于 React 实现这个功能呢?


这里的关键就在于,我们要回溯之前的状态,因此一个常规的思路就是,我在内存中,把每一次操作之后,对应的状态以快照的形式存起来。例如,我们编辑一篇文章

state1: 今天
state2: 今天天
state3: 今天天气
state4: 今天天气不
state5: 今天天气不错
state5: 今天天气不错!

这样存起来之后,你想要撤回到前一步的状态,就非常简单。因为都存在那里,我们只需要找出来就可以了。但是当文章内容变得越来越多,越来越多的时候,问题就出现了。


存储空间里,冗余的信息太多了。导致了越到后面,对存储空间的消耗就越大,但是带来的收益又非常低。因此,这种思路只适合编辑内容比较小的项目,无法运用在文章内容的编辑里,因为开发者无法预测用户一篇文章到底有多少字


此时我们需要转换思维。一个新的思路就是,我们只存储当前操作的内容,然后根据上一个完整的内容去整合出最新内容


例如,完整的内容我们初始化为

state: ''

一个操作内容我们记录为

action: {
  type: '添加',
  content: '今天'
}

这样,我们就可以结合 state 与 action,整合出来最新的 state

state = state + action.content

当你再继续输入的时候,我们用同样的办法结合现在的 state 与 新的 action,整合最新的 state

// 上次整合的结果
state = '今天'
action = {
  type: '添加',
  content: '天气'
}

整合结果

state = '今天天气'

再次输入一次操作

action = {
  type: '添加',
  content: '不行'
}

整合结果

state = '今天天气不行'

你发现写错了,因此你需要撤销一个步骤,此时,有两种思路,一种是我们用同样的方式记录你的撤销操作,然后根据操作类型去你刚才存的新增 action 类型列表里找到你要撤销的内容,用最新的状态减去操作内容即可

// 此时就只有一个操作类型,没有对应的数据
action = {
  type: '撤销'
}
state = state - preAction.content

也可以不用记录这次撤销操作,而是直接减也行,这根据你的需求来定。


如果你理解了这个场景,那么你也就理解了 redux,接下来,我们来学习一下 useReducer 的基础语法,他与 redux 几乎一模一样。


useReducer


在上面的场景中,我们需要记录一个操作,这个操作我们称之为 action. 在 action 中,我们往往会包括该操作的具体方式,以及对应的具体内容

action = {
  type: 'add',
  content: 'hello world'
}

执行 action 的操作,我们通常称之为 dispatch


我们还需要一个根据 action 整合最新状态内容的聚合方式,在 redux 中,我们称之为 reducer


因此,useReducer 的语法为

const [state, dispatch] = useReducer(reducer, initialArg, init?)

initialArg 表示状态的初始值


init 是一个需要返回初始状态的初始化函数。如果未指定,那么初始状态就设定为 initialArg,如果指定了 init,初始状态将会采用 init(initialArg) 的执行结果


在使用层面,我们只需要想办法定义好 action 的具体内容和 reducer 的具体聚合方式,然后使用 dispatch 去执行 action 即可

dispatch({
  type: 'add',
  content: 'hello world'
})

我们使用一个简单的案例来了解他们的具体使用

image.png


具体的需求是,当你点击按钮时,字符串中的数字会增加。


我们首先考虑初始状态,将其设定为 18 岁

{age: 18}

然后,目前只有一种改变方式:增加岁数,因此,我们设定 action 表示增加 1 岁,代码表示具体为

action = {
  type: 'increment',
  age: 1
}

通常我们会在更复杂的操作场景中,将 action.type 设置为 increment/age,更贴近语义


我们要根据 state 与 action,集合出最新的 state,因此聚合的方式定义为

function reducer(state, action) {
  if (action.type === 'increment') {
    return {
      age: state.age + action.age
    }
  }
}

最后在点击时,执行 action

onClick = () => {
  dispatch({ 
    type: 'increment',
    age: 1
  })
}

完整代码为

import { useReducer } from 'react';
function reducer(state, action) {
  if (action.type === 'increment') {
    return {
      age: state.age + action.age
    }
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 18 });
  return (
    <>
      <button onClick = () => {
        dispatch({ 
          type: 'increment',
          age: 1
        })
      }>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}


稍微复杂一点的案例


初始时有一个列表,在 input 中,我们可以新增列表,具体的操作如下图所示。


scroll.gif


首先,我们要约定初始状态,他包括一个列表,还需要存储输入的内容。因此他至少应该有两个字段

state = {
  draft: '',
  todos: []
}

由于初始时,列表已经存在,因此我们可以约定一个方式去自己创造列表数据

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

此时的操作有两个,一个是更改存储的草稿内容。一个是新增一项更改列表,因此我们设计 action 为

{
  type: 'changed_draft',
  nextDraft: e.target.value
}
// 内容从草稿状态中获取即可
{
  type: 'added_todo'
}

reducer 则为

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

完整代码如下

import { useReducer } from 'react';
function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}
function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}
export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    username,
    createInitialState
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


变化


这个时候,你基本上已经掌握了 useReducer,但是这个解决方案是可以应对大型项目的。因此刚才我们讲的每一个点都有可能变得更加复杂。


当 action 变得更多更复杂时,我们并不想自己去手写完整的 action 内容,因此这个时候就有一种方式,写一个函数,去创建 action,以简化 action 的使用

function createAction(age) {
  return {
    type: 'increment/age',
    age: age
  }
}

这个创建 action 的方法,我们称之为 actionCreator


当状态变得更复杂时,他有非常多的 key 值,每一个 key 可能都是对应一个页面的数据,因此我们会单独新起一个或者多个模块来管理这些复杂的 state,我们称这个单独的模块为数据中心 Store


当状态变得更加复杂,那么 reducer 的内部逻辑也会变得更加复杂,因此我们也会根据实际情况将 reducer 进行拆分,分散在不同的模块中去管理,最后再将他们合并在一起,因此就会引入一个新的概念合并 reducer combineReducers


因此,useReducer 能够结合 useContext 完成更复杂的状态管理。


注意事项


useState 就是基于 useReducer 实现而来,因此 dispatch 与 setState 有几乎相同的表现。他是一个异步行为,当为什么调用 dispatch 时,如果直接访问 state 的数据,无法拿到最新的 state 数据

function handleClick() {
  console.log(state.age);  // 18
  dispatch({ type: 'incremented_age' }); // Request a re-render with 19
  console.log(state.age);  // Still 18!
  setTimeout(() => {
    console.log(state.age); // Also 18!
  }, 5000);
}

当 state 数据变得复杂时,在 reducer 中,我们可以使用展开运算符来聚合数据,这里一定要返回一个新的数据,而不要基于之前的 state 去做修改

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        ...state, // Don't forget this!
        age: state.age + 1
      };
    }


总结


useReducer 由于使用比较繁琐,因此在应用层面我们会很少使用到他,但是,当你能力变得越来越强,需要封装一个功能更为强大的状态管理工具时,或者解决大型项目中的特定场景时,你一定会需要到它。因此在后面的学习中,我们还需要结合 useContext 进一步学习 redux.

相关文章
|
3月前
|
前端开发 API UED
Python后端与前端交互新纪元:AJAX、Fetch API联手,打造极致用户体验!
Python后端与前端交互新纪元:AJAX、Fetch API联手,打造极致用户体验!
121 2
|
4月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
3月前
|
API
本地hook API MessageBoxA的masm32源代码[07-10更新]
本地hook API MessageBoxA的masm32源代码[07-10更新]
|
2月前
|
移动开发 前端开发 JavaScript
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
298 0
|
3月前
|
XML 缓存 JavaScript
提升对前端的认知,不得不了解Web API的DOM和BOM
该文章强调了在前端开发中理解和掌握DOM(文档对象模型)和BOM(浏览器对象模型)的重要性,并介绍了它们的相关操作和应用。
提升对前端的认知,不得不了解Web API的DOM和BOM
|
4月前
|
JSON 前端开发 API
构建前端防腐策略问题之更新getMemoryUsagePercent函数以适应新的API返回格式的问题如何解决
构建前端防腐策略问题之更新getMemoryUsagePercent函数以适应新的API返回格式的问题如何解决
构建前端防腐策略问题之更新getMemoryUsagePercent函数以适应新的API返回格式的问题如何解决
|
4月前
|
JavaScript 前端开发 API
Vue.js 3.x新纪元:Composition API引领潮流,Options API何去何从?前端开发者必看的抉择指南!
【8月更文挑战第30天】Vue.js 3.x 引入了 Composition API,为开发者提供了更多灵活性和控制力。本文通过示例代码对比 Composition API 与传统 Options API 的差异,帮助理解两者在逻辑复用、代码组织、类型推断及性能优化方面的不同,并指导在不同场景下的选择。Composition API 改善了代码可读性和维护性,尤其在大型项目中优势明显,同时结合 TypeScript 提供更好的类型推断和代码提示,减少错误并提升开发效率。尽管如此,在选择 API 时仍需考虑项目复杂性、团队熟悉度等因素。
59 0
|
5月前
|
开发框架 前端开发 JavaScript
循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理
循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理
|
5月前
|
存储 开发框架 前端开发
循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用
循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用
|
5月前
|
前端开发 API UED
Python后端与前端交互新纪元:AJAX、Fetch API联手,打造极致用户体验!
【7月更文挑战第15天】Python后端(Django/Flask)与前端通过AJAX或Fetch API实现异步交互,提升Web应用体验。Python提供强大的后端支持,AJAX用于不刷新页面的数据交换,Fetch API作为现代标准,基于Promise简化HTTP请求。结合两者,构建高效、流畅的交互系统,优化响应速度和用户体验,开启Web开发新篇章。
92 5