弥合 React 的 useState、useReducer 和 Redux 之间的差距

2025-06-07

弥合 React 的 useState、useReducer 和 Redux 之间的差距

照片中,一名男子漫不经心地跳过一道很深的裂缝,由亚历克斯·拉德利奇 (Alex Radelich) 拍摄

最初发表于leewarrick.com/blog

Redux 是我视为“个人珠穆朗玛峰”的技术之一。每次看到它,我都觉得要记住的样板代码和模式无穷无尽。

在我的第一份工作以及我参与的第一个代码库中,我们必须使用 NGRX(Angular 版的 Redux)。这极具挑战性;我花了几个小时阅读文档和观看教程,试图理解 NGRX。我甚至拼命尝试学习 Redux,试图理解 NGRX。我经常向老板抱怨要记住的所有样板代码、文件和模式。

他告诉我,“如果你使用自己的解决方案,你最终可能会重复同样的模式”。

我终于承认了。在尝试了除Redux 之外的所有 React 状态管理方法后,我终于开始欣赏它的工作方式以及它需要如此多的样板代码。在学习了 React 的 Context APIuseReducer以及更多关于状态管理的知识后,我终于欣赏了 Redux。

useState然而,从 A 到 B 并不容易。学习和之间有很多内容需要学习useReducer,而当你学习 Redux 并管理复杂的状态时,学习内容就更多了。

useState 钩子

ReactuseState用起来很爽。给它一个初始值,它会返回一个响应值的引用,以及一个用于更新该值的 setter 函数。

下面是经典的反例useState

注意:如果您想查看这些示例的实际版本,请在此处查看原始帖子。

function Counter() {
    const [count, setCount] = React.useState(0)
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

够简单了!只有两个怪癖:

首先,你必须为函数提供一个新的状态值setCountsetCount(count++)count++是行不通的)。React 秉承了不变性,这意味着你应该始终返回一个新值,而不是更改当前值。

另一个怪癖是返回的数组,但几乎所有钩子都遵循这个模式。考虑到编写函数式钩子组件比编写类组件更容易,这个代价并不大。

虽然useState看起来很简单,但当你需要多个状态值时该怎么办?如果你有一个包含多个字段的表单怎么办?

幸运的是,有了钩子,我们可以useState多次使用:

function FormExample() {
  const [email, setEmail] = React.useState('')
  const [name, setName] = React.useState('')

  const columnStyle = {
    display: 'flex',
    flexDirection: 'column',
  }
  return (
    <form style={{ ...columnStyle, width: '300px' }}>
      <label style={columnStyle}>
        <span>Name:</span>
        <input
          onChange={e => setName(e.target.value)}
          value={name}
          type="text"
        />
      </label>
      <label style={columnStyle}>
        <span>Email:</span>
        <input
          onChange={e => setEmail(e.target.value)}
          value={email}
          type="text"
        />
      </label>
      <pre>{JSON.stringify({name, email}, null, 2)}</pre>
    </form>
  )
}
render(FormExample)

Enter fullscreen mode Exit fullscreen mode

太好了!但是多少才算太多呢useState?有没有合理的限制?我们应该控制在5个或更少吗?

如果您需要管理更复杂的数据结构或执行副作用怎么办?

useReducer 钩子

加里·桑多兹 (Gary Sandoz) 拍摄的一名男子搅拌篝火烹饪锅的照片

现在我们进入useReducer正题了。'useReducer' 中的 reducer 来自 Redux,而 Redux 又借用了 JavaScript 的Array.reduce()

那么“还原”是什么意思呢?想象一下,用文火慢炖香醋,醋蒸发后,留下一层香甜可口的釉料。这叫做“香醋还原”。把还原剂想象成把物质加热,直到它们更容易被接受。

在 React 的上下文中,以下是使用的典型模式useReducer

const reducer = function (currentState, action) {
  // Make a new state based on the current state and action
  // Note: There's usually a big switch statement here
  return newState
}
const [state, dispatch] = useReducer(reducer, initialValue)

// example usage:
dispatch({type: "THING_HAPPENED"})
// Or with an optional "payload":
dispatch({type: "THING_HAPPENED", payload: newData})

Enter fullscreen mode Exit fullscreen mode

在深入探讨经典的 Reducer 模式之前,我想先概括useReducer一下它的基本功能。简而言之:useReducer它与 几乎完全相同useState,只不过useReducer它允许你通过传递一个函数来精确定义如何更新其状态值。

让我们看看之前的反例。这里我们将useState用 来实现我们自己的反例useReducer

function Counter() {
    const [count, setCount] = React.useReducer((currentCount, newCount) => newCount, 0)
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

但这纯属额外工作,没有任何好处。为什么要把一个函数传递给useReducer,又把另一个函数传递给onClick?而且,我们的计数器逻辑放在了 JSX 按钮元素里,这不太好。

让我们删除多余的功能并将逻辑移出 JSX:

function Counter() {
    const [count, increment] = React.useReducer(currentCount => currentCount + 1, 0)
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>
                Increment
            </button>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

如果你没有从本文中学到任何其他东西,请记住这一点:

它的强大之处useReducer在于它允许我们定义如何更新我们的状态值。

也就是说,在我们进一步深入研究减速器和模式之前,我想花一点时间来定义“状态”。

React 中的“状态”问题

关于 React 中的“状态”,存在一些重大误解。我认为,当 Vue 将其状态版本命名为“数据”时,是为了更容易理解 Vue 代码。

React 定义的状态实际上只是我们通常存储在变量中的数据。然而,React 需要对这些数据的变化做出反应。因此,当你将数据存储在状态中时,React 会在底层将方法和属性附加到该对象上,以便它知道何时触发重新渲染。

React“状态”只是 React 监视更新的对象。

那么,如果 React 的“状态”不是真正的状态,那是什么呢?“状态”的概念实际上比 React 早了几十年。用计算机科学术语来说,应用程序的状态描述了其当前状态以及导致该状态的先前事件和用户交互。

这种状态在编程中处理起来非常困难。这就是为什么每个技术支持人员在你寻求帮助时都会默认“关闭并重新打开”。你的设备不知何故进入了错误状态,有时最简单的摆脱错误状态的方法就是重启系统,使其恢复到新的状态。

在编写 React 代码时,我们经常会将程序的状态与 React 渲染时需要监控的数据混淆。例如,你的组件中可能包含描述用户在输入框中输入内容的数据,也可能包含指示表单是否有效的数据。这些当前数据以及响应用户操作后数据的变化,共同构成了组件的实际状态。

我们通常只担心在组件中存储和更新数据,并且避免考虑它的实际状态,直到我们开始发现错误。

Reducers 和 Redux

Reducer 模式旨在简化复杂状态更新流程。虽然并非万无一失或简单易行,但它可以帮助我们定义和管理应用程序和组件中的状态变化。

让我们看一下表单上下文中 Reducer 模式的简单版本:

const reducer = function (currentState, action) {
    switch(action.type) {
        case 'NAME_CHANGED':
            return {...currentState, name: action.payload}
        case 'EMAIL_CHANGED':
            return {...currentState, email: action.payload}
        default:
            return state
    }
}
const [state, dispatch] = useReducer(reducer, {name: '', email:''})

// example usage:
dispatch({type: 'NAME_CHANGED'})
// or with a payload:
dispatch({type: 'NAME_CHANGED', payload: 'Suzy'})

Enter fullscreen mode Exit fullscreen mode

可以将其视为一个事件信号系统。当我们调用 时dispatch,我们传入一个对象来告诉我们发生了什么,然后我们的 Reducer 会获取该信息并进行处理以创建一个新的状态。

那么为什么叫它 dispatch 和 action 呢?为什么要用 switch 语句呢?

调度员

我喜欢把它想象dispatch成一个老式电话总机系统的调度员。调度员将信息与主消息(类型)以及任何附加信息(有效载荷)打包在一起,然后将其插入到总机,也就是我们的 Reducer(恰好包含一个switch)。

行动

他们真的应该把它们叫做“事件”而不是“动作”。动作描述的是应用程序中发生的事件。因此,在命名动作类型时,最好使用过去时,例如"NAME_CHANGED",而不是现在时"CHANGE_NAME"

虽然这看起来语义上无关紧要,但它对理解 Redux 模式至关重要。务必记住,你的 Reducer 会响应事件来决定新的状态。当你说 时"CHANGE_NAME",你是在暗示你的 Reducer更改名称,而不是让它自己决定是否更改。

注意:虽然我更倾向于将其称为事件,但为了方便起见,我们还是使用“动作”吧。只需记住在动作类型中使用过去时即可。

另注:我们的操作类型也使用了SCREAMING_SNAKE_CASE命名方式。这是为了表明字符串是常量,同时也提醒您不要修改它们。(顺便说一句,“Screaming Snake Case” 这个名字很适合金属乐队。)

Switch 语句

选择 switch 语句而不是长if/else if链主要是为了可读性。

你可能还会注意到,我们的 switch 语句中没有break语句,但有很多展开运算符。我们用returnbreak 代替了 break,这样可以避免 switch 瀑布效应(稍后会详细介绍)。至于展开运算符,请记住 React 是建立在不可变性基础上的,因此创建新对象是必要的。通过先展开,然后再传递更改,我们可以只覆盖 state 中需要的属性,而不会影响其他属性:

const state = {
  name: "Robert",
  email: "SuperBobby74@aol.com"
}
const newState = {...state, name: "Bobby"}
console.log(newState)

Enter fullscreen mode Exit fullscreen mode

让我们将 Reducer 模式应用useReducer到我们之前的表单中:

function FormExample() {
  function formReducer(state, action) {
    switch (action.type) {
      case 'NAME_CHANGED':
        return { ...state, name: action.payload }
      case 'EMAIL_CHANGED':
        return { ...state, email: action.payload }
      default:
        return state
    }
  }

  const [state, dispatch] = React.useReducer(formReducer, {
    name: '',
    email: '',
  })

  const columnStyle = {
    display: 'flex',
    flexDirection: 'column',
  }

  return (
    <form style={{ ...columnStyle, width: '300px' }}>
      <label style={columnStyle}>
        <span>Name:</span>
        <input
          onChange={e =>
            dispatch({ type: 'NAME_CHANGED', payload: e.target.value })
          }
          value={state.name}
          type="text"
        />
      </label>
      <label style={columnStyle}>
        <span>Email:</span>
        <input
          onChange={e =>
            dispatch({ type: 'EMAIL_CHANGED', payload: e.target.value })
          }
          value={state.email}
          type="text"
        />
      </label>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </form>
  )
}

render(FormExample)

Enter fullscreen mode Exit fullscreen mode

这很有效,但我们可以做一些改进。

首先,让我们提取我们的动作类型并将它们变成这样的对象:

const actions = {
  nameChanged: 'NAME_CHANGED',
  emailChanged: 'EMAIL_CHANGED',
}

Enter fullscreen mode Exit fullscreen mode

这将避免后续的错误。如果您actions.nameChanged在 switch 和 dispatch 中使用,IDE 可能会帮助您避免操作类型拼写错误造成的错误。(如果代码库使用的是 TypeScript,您可能会在枚举中看到同样的模式。)

我们还可以将初始状态拉到它自己的对象中,并将其与我们的减速器和动作一起移到我们的组件之外。

const actions = {
  nameChanged: 'NAME_CHANGED',
  emailChanged: 'EMAIL_CHANGED',
}

const initialState = {
  name: '',
  email: '',
}

function formReducer(state, action) {
  switch (action.type) {
    case actions.nameChanged:
      return { ...state, name: action.payload }
    case actions.emailChanged:
      return { ...state, email: action.payload }
    default:
      return state
  }
}

function FormExample() {
  const [state, dispatch] = React.useReducer(formReducer, initialState)

  const columnStyle = {
    display: 'flex',
    flexDirection: 'column',
  }
  return (
    <form style={{ ...columnStyle, width: '300px' }}>
      <label style={columnStyle}>
        <span>Name:</span>
        <input
          onChange={e =>
            dispatch({ type: actions.nameChanged, payload: e.target.value })
          }
          value={state.name}
          type="text"
        />
      </label>
      <label style={columnStyle}>
        <span>Email:</span>
        <input
          onChange={e =>
            dispatch({ type: actions.emailChanged, payload: e.target.value })
          }
          value={state.email}
          type="text"
        />
      </label>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </form>
  )
}
render(FormExample)

Enter fullscreen mode Exit fullscreen mode

使用 Reducer 处理业务逻辑

你可能会好奇,我们为什么要把useState示例弄得这么复杂?看起来我们所做的只是添加代码来重复之前的功能。

当我们在表单中添加提交按钮时,Reducer 才真正开始发挥作用。表单本身就极其复杂(需要管理大量状态),这就是为什么会有如此多的表单库。你需要考虑验证,还要跟踪哪些字段被填写了,提交表单时会发生什么,等等。

如果你打算用 来管理这些逻辑useState,你会发现你需要用大量的代码来包装你的提交,添加更多的useState钩子,甚至可能将你的 setter 函数包装在可能更新其他状态值的验证函数中。这很快就会变得混乱。

与 不同useStateuseReducer它提供了一个很好的基础架构来处理围绕验证和提交的所有逻辑:

const actions = {
  nameChanged: 'NAME_CHANGED',
  emailChanged: 'EMAIL_CHANGED',
  formSubmitted: 'FORM_SUBMITTED',
}

const initialState = {
  name: '',
  email: '',
  nameError: null,
  emailError: null,
  formCompleted: false,
  formSubmitted: false,
}

function formReducer(state, action) {
  let error
  switch (action.type) {
    case actions.nameChanged:
      error = validate('name', action.payload)
      return { ...state, name: action.payload, nameError: error }
    case actions.emailChanged:
      error = validate('email', action.payload)
      return { ...state, email: action.payload, emailError: error }
    case actions.formSubmitted:
      // if the form has been successfully submitted,
      // stop here to prevent rage clicks and re-submissions
      if (state.formCompleted) return state
      let formValid = true
      // invalidate the form if values are missing or in error
      if (state.nameError || !state.name || state.emailError || !state.email) {
        formValid = false
      }
      // if the user has attempted to submit before, stop here
      if (state.formSubmitted) return { ...state, formCompleted: formValid }
      // if this is the first submit, we need to validate in case the user
      // clicked submit without typing anything
      let nameError = validate('name', state.name)
      let emailError = validate('email', state.email)
      return {
        ...state,
        nameError,
        emailError,
        formSubmitted: true,
        formCompleted: formValid,
      }
    default:
      return state
  }
}

// this helper function validates the name and email inputs
// if there's an error, it returns an error message describing the problem
// if there are no errors, it returns null
// it's outside our reducer to make things more readable and DRY
function validate(name, value) {
  if (typeof value === 'string') value = value.trim()
  switch (name) {
    case 'name':
      if (value.length === 0) {
        return 'Must enter name'
      } else if (value.split(' ').length < 2) {
        return 'Must enter first and last name'
      } else {
        return null
      }
      break
    case 'email':
      if (value.length === 0) {
        return 'Must enter email'
      } else if (
        !value.includes('@') ||
        !value.includes('.') ||
        value.split('.')[1].length < 2
      ) {
        return 'Must enter valid email'
      } else {
        return null
      }
      break
  }
}

function FormExample() {
  const [state, dispatch] = React.useReducer(formReducer, initialState)

  // extract our dispatch to a change handler to DRY the code up
  function handleChange(e) {
    dispatch({ type: actions[e.target.name + 'Changed'], payload: e.target.value })
  }

  // this is attached to the form, not the submit button so that
  // the user can click OR press 'enter' to submit
  // we don't need a payload, the input values are already in state
  function handleSubmit(e) {
    e.preventDefault()
    dispatch({ type: actions.formSubmitted })
  }

  const columnStyle = {
    display: 'flex',
    flexDirection: 'column',
  }
  // this adds a red outline to the input if the field isn't filled out correctly,
  // but only if the user has attempted to submit
  const inputStyle = hasError => {
    return {
      outline: hasError && state.formSubmitted ? '2px solid red' : 'none',
    }
  }
  return (
    <form style={{ ...columnStyle, width: '300px' }} onSubmit={handleSubmit}>
      <label style={columnStyle}>
        <span>Name:</span>
        <input
          style={inputStyle(state.nameError)}
          onChange={handleChange}
          name="name"
          value={state.name}
          type="text"
        />
        <span>{state.formSubmitted && state.nameError}</span>
      </label>
      <label style={columnStyle}>
        <span>email:</span>
        <input
          style={inputStyle(state.emailError)}
          onChange={handleChange}
          name="email"
          value={state.email}
          type="text"
        />
        <span>{state.formSubmitted && state.emailError}</span>
      </label>
      <p>{state.formCompleted && 'Form Submitted Successfully!'}</p>
      <button type="submit">Submit</button>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </form>
  )
}

render(FormExample)

Enter fullscreen mode Exit fullscreen mode

注意我们的 Reducer 函数是如何随着业务逻辑而膨胀的。没关系!事实上,保持 Reducer 函数臃肿、事件处理器精简是一个好的经验法则。

功能也发生了变化。随着值的变化,reducer 会处理验证,并在必要时将错误消息添加到状态中。如果表单尚未提交,我们可以暂时不显示红框和错误消息来烦扰用户,直到他们提交为止。如果提交时出现错误,我们可以在用户输入时更改消息,以引导他们输入正确的信息。最后,我们可以formCompleted在提交案例中添加一个标记,以防止恶意点击和重复提交。

这为用户提供了出色的体验,并为所有这些复杂的 UI 交互提供了良好的组织模型。

欢迎来到 Redux

信不信由你,我们现在已经实现了 Redux 的所有主要组件。Redux 本身实际上只是一个辅助库,用于帮助我们完成本文中所做的相同操作。

在典型的 Redux 应用中,我们会将actionsreducersstate分别放到项目中各自的文件中。为了管理多个状态对象,我们可以将 actions/reducers/state 的集合分组到不同的store中,然后这些 store 会通过一个根 reducer组成一个全局 store。根 reducer 的作用是将每个 store 的状态组合成一个全局状态对象。

然后,我们将所需的 store、dispatcher 和 action 导入到组件中,以便访问状态并将事件发送到全局 store。Redux 提供了一些实用程序来帮助构建这个全局状态管理系统,但大多数情况下,您需要自己编写所有 action、reducer 和状态,就像我们在本文中所做的那样。

所以,如果你读到这里,你已经准备好使用 Redux 了!真正的问题是,你应该这么做吗?

Redux 死了吗☠?

如今,有了 Context API 和本文所学的知识,即使没有 Redux,你也能做很多事情。你可以将 Context 想象成一个 Redux Store,它可以放置在应用中的任何位置。任何被 Context Provider 包装的组件都可以访问你从中共享的值。Context 可以位于应用的顶层,为所有组件提供状态;也可以位于更底层,只与少数组件共享其状态。

Kent C Dodds 有一篇关于在 React 中使用 Context 进行状态管理的优秀 文章。

话虽如此,Redux 并没有消亡。现在有很多代码库在使用它,如果你想专业地编写 React 代码,学习它不失为一个好主意。

超越 Redux 😵

我们现在要讨论一些稍微高级的话题,所以请系好安全带。

即使是最敏锐的测试人员也可能注意到了上一个示例中的 bug。向上滚动页面,看看是否能找到我们遗漏的这个边缘情况。

放弃?

成功提交后您可以编辑表单!

我们该如何解决这个问题?你的第一反应可能是开始formSubmitted在整个 reducer 中放置标志,以防止对表单进行进一步的更改,就像我们在提交案例开始时所做的那样。

这虽然可行,但读起来和推理起来都比较困难。我认为提交的情况本身就已经有点乱了,在其他情况里再加逻辑只会让情况更糟。

更重要的是,我们一开始怎么就忽略了这一点?我们学习了所有这些复杂的 JavaScript 来防止 bug,但我们还是发现了一些 bug!

隐式状态与显式状态

在关于状态的切线部分,我提到我们有时会在代码中使用布尔值或标志来描述状态。我们在表单中使用formCompleted和 来实现了这一点formSubmitted。问题在于,我们隐式地描述了表单的状态,而不是显式地描述。

这意味着我们依赖这些布尔值的组合来描述表单的状态。例如,如果用户没有输入任何内容,也没有点击提交,我们可以这样写:

if (!formSubmitted && !name && !email && !emailError && !nameError) {
  // behave as if user hasn't done anything yet
}

Enter fullscreen mode Exit fullscreen mode

这很混乱,难以理解。以后再看这段代码时,你甚至可能会忘记它是如何工作的,并且犹豫是否要修改它。更好的方法是明确描述表单的状态,然后确保表单在任何时间点只能处于其中一种状态。

我们可以将表单状态描述为:

  • 清洁- 用户尚未输入任何内容或按下提交
  • - 用户已开始输入信息,但尚未成功完成并提交
  • 已完成- 表格已正确填写并提交

我们还希望处理这些状态之间的转换以及每个状态下可能发生的操作:

清洁- 用户尚未输入任何内容或按下提交

  • 可能的转变:脏

- 允许的操作:编辑和提交,但提交不会触发错误,只会显示一条消息

- 用户已开始输入信息,但尚未成功完成并提交

  • 可能的转变:已完成

- 允许的操作:编辑和提交,但提交会触发错误消息

已完成- 表格已正确填写并提交

  • 可能的转变:无!
  • 允许的操作:无!

有限状态机

我们刚刚创建的思维模型是一个状态机,或者说有限状态机(FSM)。有限意味着表单可以存在的状态数量有限,状态描述表单的状态,机器指的是我们如何在不同状态之间转换的机制。

我不是状态机专家,因此我强烈建议阅读David Khourshid 的这些 文章,以深入了解 FSM。

有两种选项可以将此模型应用到我们的代码中。

首先,有一个专为有限状态机 (FSM) 量身定制的库,名为XState,由上面提到的 David 编写。如果你感兴趣的话,Dave Geddes 还提供了一篇关于在 React 中使用 xstate 的精彩教程。

另一种选择是自己在 Reducer 中实现逻辑。这有点难,但如果你读过我链接的 FSM 文章,你可能见过一个用嵌套 switch语句实现的 FSM 的例子。让我们把它应用到表单中。

高级 Switch 语句

在我们开始最后一个例子之前,让我们简单回顾一下 JavaScript 的switch

我们将要使用“fall-through”或“瀑布”切换用法。这意味着我们不会故意每种情况下都使用break,以便能够匹配多种情况。

让我们看一个例子,我们不顾妈妈的建议,不吃早餐,但仍然吃午餐和晚餐:

const actionType = "LUNCH_ORDERED"

switch(actionType) {
  case "BREAKFAST_ORDERED":
    console.log("breakfast")
    // no break!
  case "LUNCH_ORDERED":
    console.log("lunch")
    // no break!
  case "DINNER_ORDERED":
    console.log("dinner")
    break
  default:
    console.log("fasting 😵")
}

Enter fullscreen mode Exit fullscreen mode

一旦匹配了一个案例,您就会匹配所有案例,直到您打破或返回。

那么嵌套开关呢?

function dailyLife(status, actionType) {
  switch(status) {
    case "work":
      switch(actionType) {
        case "WORK_REQUESTED":
          console.log("DOING WORK")
          break
      }
    //no break after "work"
    case "holiday":
      switch(actionType) {
        case "CAKE_EATEN":
          console.log("FEELING FAT")
          break
        case "NAP_REQUESTED":
          console.log("NAPPING")
          break
      }
  }
}
console.log("ooooh, who's birthday is it?")
dailyLife("work", "CAKE_EATEN") // feeling fat

console.log("Taking a break, afk")
dailyLife("work", "NAP_REQUESTED") // napping

console.log("Hey, I know it's Saturday, but can you get us that TPS report?")
dailyLife("holiday", "WORK_REQUESTED") // not happening, sorry boss

Enter fullscreen mode Exit fullscreen mode

这里我们可以看到,你可以在工作时间和假期午睡,但你不能在假期工作。(至少你不应该)。

这个想法是,如果你必须在状态之间共享操作,那么将未共享操作的状态放在顶部。如果我们只能在工作时间工作,那么工作状态应该放在顶部。如果你可以在工作时间和假期吃蛋糕,那么假期/吃蛋糕的状态应该放在底部。

这绝对是一种先进的技术,因此在编写嵌套和瀑布式案例的复杂开关时要小心并经常测试。

就我们的表单而言,我们希望用户无论表单处于“干净”状态还是“脏”状态都能编辑。为了共享输入更改操作,我们不会将break干净和脏状态的表单切换为相同的操作,以便这两种状态都能使用。此外,你可以在两种状态下提交表单,但提交操作在每种状态下的行为有所不同。

好了,开始吧!让我们看一下包含 FSM 和 的最终形式示例useReducer

const actions = {
  nameChanged: 'NAME_CHANGED',
  emailChanged: 'EMAIL_CHANGED',
  formSubmitted: 'FORM_SUBMITTED',
}

const initialState = {
  name: '',
  email: '',
  nameError: null,
  emailError: null,
  submitAttempted: false,
  submitMessage: '',
  status: 'clean',
}

function formReducer(state, action) {
  let error
  switch (state.status) {
    case 'dirty':
      switch (action.type) {
        case actions.formSubmitted:
          let formValid = true
          let nameError = validate('name', state.name)
          let emailError = validate('email', state.email)
          if (nameError || !state.name || emailError || !state.email) {
            formValid = false
          }
          return {
            ...state,
            nameError,
            emailError,
            submitAttempted: true,
            status: formValid ? 'completed' : 'dirty',
            submitMessage: formValid
              ? 'Form Submitted Successfully'
              : 'Form Has Errors',
          }
      }
    // no 'break' or 'return', case 'dirty' continues!
    case 'clean':
      switch (action.type) {
        case actions.nameChanged:
          error = validate('name', action.payload)
          return {
            ...state,
            name: action.payload,
            nameError: error,
            submitMessage: '',
            status: 'dirty',
          }
        case actions.emailChanged:
          error = validate('email', action.payload)
          return {
            ...state,
            email: action.payload,
            emailError: error,
            submitMessage: '',
            status: 'dirty',
          }
        case actions.formSubmitted:
          return {
            ...state,
            submitMessage: 'Please fill out the form',
          }
        default:
          return state
      }
    case 'completed':
    // no 'break' or 'return', case 'completed' continues!
    default:
      return state
  }
}

function validate(name, value) {
  if (typeof value === 'string') value = value.trim()
  switch (name) {
    case 'name':
      if (value.length === 0) {
        return 'Must enter name'
      } else if (value.split(' ').length < 2) {
        return 'Must enter first and last name'
      } else {
        return null
      }
      break
    case 'email':
      if (value.length === 0) {
        return 'Must enter email'
      } else if (
        !value.includes('@') ||
        !value.includes('.') ||
        value.split('.')[1].length < 2
      ) {
        return 'Must enter valid email'
      } else {
        return null
      }
      break
  }
}

function FormExample() {
  const [state, dispatch] = React.useReducer(formReducer, initialState)

  function handleChange({ target: { name, value } }) {
    dispatch({ type: actions[name + 'Changed'], payload: value })
  }

  function handleSubmit(e) {
    e.preventDefault()
    dispatch({ type: actions.formSubmitted })
  }

  const columnStyle = {
    display: 'flex',
    flexDirection: 'column',
  }
  const inputStyle = hasError => {
    return {
      outline: hasError && state.submitAttempted ? '2px solid red' : 'none',
    }
  }
  return (
    <form style={{ ...columnStyle, width: '300px' }} onSubmit={handleSubmit}>
      <label style={columnStyle}>
        <span>Name:</span>
        <input
          style={inputStyle(state.nameError)}
          onChange={handleChange}
          name="name"
          value={state.name}
          type="text"
        />
        <span>{state.submitAttempted && state.nameError}</span>
      </label>
      <label style={columnStyle}>
        <span>email:</span>
        <input
          style={inputStyle(state.emailError)}
          onChange={handleChange}
          name="email"
          value={state.email}
          type="text"
        />
        <span>{state.submitAttempted && state.emailError}</span>
      </label>
      <p>{state.submitMessage}</p>
      <button type="submit">Submit</button>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </form>
  )
}

render(FormExample)

Enter fullscreen mode Exit fullscreen mode

现在我们的表单没有错误了!

我们已经明确地建模并解释了它可能存在的所有状态,并定义了这些状态下可能采取的行动。

注意:submitAttempted你可能会注意到代码中仍然有一个布尔值。这没问题,因为它仅用于显示或隐藏表单中的错误消息。最重要的是,我们不会检查submitAttempted当前状态。

临别感想

这篇文章充满了高级概念,我希望即使你没有读到最后,也能学到一些。如果你没有理解每一个概念和示例,也不用担心。从简单的开始,先在自己的代码中应用和练习这些概念,然后再学习更难的概念。我就是这样学习的。

感谢您阅读这篇长文,加油!

喜欢这篇文章吗?请订阅我的新闻通讯收听我的播客!

文章来源:https://dev.to/leewarrickjr/bridging-the-gap- Between-react-s-usestate-usereducer-and-redux-3k94
PREV
6步学会编程
NEXT
如何在 Vercel 上部署 NestJS 应用