前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >这个 hook api,曾吓退许多前端开发者

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

作者头像
用户6901603
发布2023-12-19 19:44:51
1360
发布2023-12-19 19:44:51
举报
文章被收录于专栏:不知非攻不知非攻

React 知命境」第 27 篇

在 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 实现这个功能呢?

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

代码语言:javascript
复制
state1: 今天
state2: 今天天
state3: 今天天气
state4: 今天天气不
state5: 今天天气不错
state5: 今天天气不错!

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

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

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

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

代码语言:javascript
复制
state: ''

一个操作内容我们记录为

代码语言:javascript
复制
action: {
  type: '添加',
  content: '今天'
}

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

代码语言:javascript
复制
state = state + action.content

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

代码语言:javascript
复制
// 上次整合的结果
state = '今天'

action = {
  type: '添加',
  content: '天气'
}

整合结果

代码语言:javascript
复制
state = '今天天气'

再次输入一次操作

代码语言:javascript
复制
action = {
  type: '添加',
  content: '不行'
}

整合结果

代码语言:javascript
复制
state = '今天天气不行'

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

代码语言:javascript
复制
// 此时就只有一个操作类型,没有对应的数据
action = {
  type: '撤销'
}


state = state - preAction.content

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

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

useReducer

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

代码语言:javascript
复制
action = {
  type: 'add',
  content: 'hello world'
}

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

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

因此,useReducer 的语法为

代码语言:javascript
复制
const [state, dispatch] = useReducer(reducer, initialArg, init?)

initialArg 表示状态的初始值

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

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

代码语言:javascript
复制
dispatch({
  type: 'add',
  content: 'hello world'
})

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

image.png

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

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

代码语言:javascript
复制
{age: 18}

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

代码语言:javascript
复制
action = {
  type: 'increment',
  age: 1
}

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

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

代码语言:javascript
复制

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

最后在点击时,执行 action

代码语言:javascript
复制
onClick = () => {
  dispatch({ 
    type: 'increment',
    age: 1
  })
}

完整代码为

代码语言:javascript
复制
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

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

代码语言:javascript
复制
state = {
  draft: '',
  todos: []
}

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

代码语言:javascript
复制
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 为

代码语言:javascript
复制
{
  type: 'changed_draft',
  nextDraft: e.target.value
}

// 内容从草稿状态中获取即可
{
  type: 'added_todo'
}

reducer 则为

代码语言:javascript
复制
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);
}

完整代码如下

代码语言:javascript
复制
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 的使用

代码语言:javascript
复制
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 数据

代码语言:javascript
复制
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 去做修改

代码语言:javascript
复制
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        ...state, // Don't forget this!
        age: state.age + 1
      };
    }

总结

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景
  • useReducer
  • 稍微复杂一点的案例
  • 变化
  • 注意事项
  • 总结
相关产品与服务
腾讯云微搭低代码
微搭低代码是一个高性能的低代码开发平台,用户可通过拖拽式开发,可视化配置构建 PC Web、H5 和小程序应用。 支持打通企业内部数据,轻松实现企业微信管理、工作流、消息推送、用户权限等能力,实现企业内部系统管理。 连接微信生态,和微信支付、腾讯会议,腾讯文档等腾讯 SaaS 产品深度打通,支持原生小程序,助力企业内外部运营协同和营销管理。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档