这个 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.

相关文章
|
4月前
|
前端开发 API Docker
web前端开发项目走proxy代理后端接口,构建发布到生产等环境后,如何修改api接口
web前端开发项目走proxy代理后端接口,构建发布到生产等环境后,如何修改api接口
44 0
|
12天前
|
缓存 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
【4月更文挑战第30天】Flutter允许开发者通过`CustomPaint`和`CustomPainter`进行自定义绘制,以实现丰富视觉效果。`CustomPaint` widget将`CustomPainter`应用到画布,而`CustomPainter`需实现`paint`和`shouldRepaint`方法。`paint`用于绘制图形,如示例中创建的`MyCirclePainter`绘制蓝色圆圈。Canvas API提供绘制形状、路径、文本和图片等功能。注意性能优化,避免不必要的重绘和利用缓存提升效率。自定义绘制让Flutter UI更具灵活性和个性化,但也需要图形学知识和性能意识。
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
|
15天前
|
缓存 前端开发 JavaScript
【专栏】GraphQL,Facebook 开发的API查询语言,正在前端开发中崭露头角
【4月更文挑战第27天】GraphQL,Facebook 开发的API查询语言,正在前端开发中崭露头角。它提供强类型系统、灵活查询和实时更新,改善数据获取效率和开发体验。掌握GraphQL涉及学习基础概念、搭建开发环境和实践应用。结合前端框架,利用缓存和批量请求优化性能,与后端协作设计高效API。尽管有挑战,但GraphQL为前端开发开辟新道路,引领未来趋势。一起探索GraphQL,解锁前端无限可能!
|
1月前
|
前端开发 API 数据库
Django(五):如何在Django中通过API提供数据库数据给前端
Django(五):如何在Django中通过API提供数据库数据给前端
|
2月前
|
JavaScript 前端开发 API
深入浅出Vue 3 Composition API:重塑前端开发范式
【2月更文挑战第12天】 本文旨在深入探讨Vue 3中的Composition API,一种全新的组件和逻辑复用方式。相较于传统的Options API,Composition API提供了更为灵活和高效的代码组织机制。通过实例和对比分析,我们将揭示其如何优化代码结构,提升项目的可维护性和扩展性。文章不仅为初学者铺平进入Vue 3世界的道路,也为有经验的开发者提供了深度思考的视角,探索前端开发的新范式。
|
3月前
|
前端开发 JavaScript API
前端秘法番外篇----学完Web API,前端才能算真正的入门
前端秘法番外篇----学完Web API,前端才能算真正的入门
|
7月前
|
JSON JavaScript 前端开发
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(四)完结撒花✿✿ヽ(°▽°)ノ✿
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(四)完结撒花✿✿ヽ(°▽°)ノ✿
538 0
|
7月前
|
JavaScript 前端开发 API
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)
521 1
|
7月前
|
JavaScript 前端开发 API
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(二)
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(二)
470 0
|
3月前
|
存储 前端开发 搜索推荐
前端开发中值得关注的三个Web API
【2月更文挑战第4天】Web API是前端开发中非常重要的一部分,它们为开发者提供了众多的功能和特性,帮助我们构建更加高效、优美的Web应用。本文将介绍三个值得关注的Web API,包括Web Storage、Geolocation和Web Notifications,希望能够对前端开发者有所帮助。