众所周知,Web前端开发工程师,这个职位在WEB开发行业中,是比较吃香的。技术层面的高低,决定你在这个行业的地位;要想提高自己的地位,我们必须不断的扩大自己的知识面。今天带大家了解下最近流行的技术栈:React+ TypeScript + Redux Toolkit。OK,走起吧!!!
在本文中,我将解释Redux Toolkit如何简化基于Redux的应用程序的构建,并与React和TypeScript完美地结合在一起,以实现全面的堆栈。
为什么要谈论这个?
我想在Instil-React+ TypeScript + Redux Toolkit上推广我们喜欢并成功的堆栈。但是我不确定该谈论哪些方面或向谁宣传。回顾JavaScript 2019的现状,我知道很多人正在使用React构建应用程序。
我也知道就语言而言,TypeScript非常流行(尽管不一定与React一起使用)。
从管理数据的角度来看,Redux也很受欢迎,但是下面一个有趣的数字是曾经使用Redux但不会再使用它的19.8%。
另外,从Google趋势来看,Redux的搜索量与Redux Toolkit的搜索量相比是相形见war。
因此,在本文中,我重点介绍:
-
从总体上看,去耦状态的好处
-
快速回顾Redux如何做到这一点
-
使用Redux Toolkit简化Redux
- 如果您尝试过普通的Redux并发现困难,这一点尤其重要
-
全部在React和TypeScript的上下文中呈现
我的希望是,到本文结尾,您将发现Redux的程度降低,并且鼓励您将它与TypeScript一起用于React项目。我为本文创建的示例应用程序是一个基于Web的基本计算器,
我假设您具有React和TypeScript的基本工作知识。如果您想深入了解我介绍的所有内容,请点赞并在下面发表评论。
耦合状态和UI
React是一个完全专注于构建视图的框架,它做得很好。简洁的语法和JSX视图描述符与标准编程元素的交织有助于轻松构建基于组件的UI。
export const Buttons = () =>
<div>
{buttons.map((row, rowIndex) => (
<div key={rowIndex} className='row no-gutters'>
{row.map((button, colIndex) => <Button key={colIndex} value={button}></Button>)}
</div>
))}
</div>
它不能很好地处理状态。将状态耦合到我们的视图并通过大型组件树传递状态是脆弱而笨拙的。只要组件拥有状态,就会在组件中驻留更多的业务逻辑,这使问题更加复杂。
一个更优雅的解决方案,用于将我们的状态和域逻辑与我们的视图分开。然后,我们可以任意选择要绑定到的组件,我们仅绑定所需的内容和所需的位置。
解耦后,状态逻辑和域逻辑更易于开发。消除视图顾虑可以使调试和测试更加容易,并使我们能够专注于简单的数据转换。我们可以将系统建模为数据,并根据进入系统的事件描述数据的变化。这使我们在构建更复杂的应用程序时可以大大减少认知负担。
由于组件较小,并且仅基于某些状态渲染模板,因此视图也更易于使用。重组UI,拆分或合并组件变得很容易。由于我们可以将任何状态任意绑定到视图的任何部分,因此我们可以轻松地移动和共享在树中利用状态的位置。例如,将总计篮子从屏幕底部的组件复制到顶部的工具栏,或将繁忙状态用于多个组件。
Redux刷新器
Redux是一个常见的状态管理框架,通常与React搭配使用(这也促进了Redux支持的功能性和不变性思想)。
在Redux模型中,
-
商店--包含并管理我们的状态
-
动作--描述状态变化(您可能将其视为事件)
-
一个描述更改的简单对象
-
Action Creator函数使生成它们的过程更简单
-
-
Reducer --一个纯函数,它接受当前状态,一个动作并计算下一个状态
因此,一般流程是,我们从某个初始状态开始,然后我们可以将操作分派到商店,该商店将使用化简器来计算新状态。该视图侦听商店,并在发生更改时得到通知。
无需赘述,系统依赖于状态数据结构的不变性,这就是为什么reducer是纯函数(无副作用)的原因。当我们拥有不可变的数据结构时,检查是否有所更改(因此知道UI是否必须重新渲染)是微不足道的,因为这只是一个参考比较-有所不同,它必须是一个不同的对象。
考虑到我们的计算器应用程序,我们将如何在Redux中呈现它?
-
状态-系统需要代表什么?
-
当前输入的号码
-
先前的结果
-
当前操作
-
-
动作-我们的应用程序会发生什么事件?
- 按下按钮-这些可以进一步分解,例如数字,运算,等
-
减速器--处理动作+状态以产生下一个状态
- 处理按钮按下
Redux工具包
在常规Redux中实现动作,Reducer和状态需要编写大量样板代码。归约器的纯函数和不变性约束也使事情变得更加复杂。
过去,我编写了自己的助手来缓解这种情况,但是Redux Toolkit现在可以消除您自己执行此操作的需要。它很容易添加到项目中,
npm install --save @reduxjs/toolkit
很棒的是,它包含TypeScript绑定。
Actions, Reducers, and Slices
工具包为创建标准元素(存储,还原器,操作,异步操作等)提供了帮助。尽管切片的概念更加有用,但它使我们可以在一个容器中设置状态,还原器和操作。
const slice = createSlice({
name: 'some-name',
initialState: {
// ...
},
reducers: {
// ...
}
});
"reducers"对象定义既定义动作又针对该特定动作的归约逻辑的函数。此外,reduceer函数可以以不可变的样式编写,返回新状态,或者可以以可变的样式编写。
该工具包使用另一个库Immer,因此可以使用简单的突变来编写函数。在后台,Immer将传递一个代理状态对象,跟踪更改,然后执行所需的不可变转换。这可以大大简化某些操作,例如更改深层嵌套的对象结构或数组以及其他数据结构。
切片生成纯归约化函数以及可导出的动作创建器函数的集合。例如,使用我们的计算器,代码:
export interface State {
value: string;
operation?: Operation;
previousValue?: string;
}
const slice = createSlice({
name: 'calculator',
initialState: {
value: '0',
previousValue: undefined,
operation: undefined
} as State,
reducers: {
keyPressed(state: State, {payload: key}: PayloadAction<string>) {
// ...
}
}
});
export const reducer = slice.reducer;
export const {keyPressed} = slice.actions;
所有这些的好处是它是类型安全的(并且没有太多额外的注释)。生成的动作创建者(例如keyPressed)将根据化简器部分中的函数定义采用正确的参数类型。
keyPressed(state: State, {payload: key}: PayloadAction<string>) {
// ...
}
...
keyPressed('3'); // Valid
keyPressed(3); // Invalid
还可以在" createSlice"参数的" extraReducers"部分中包括在外部创建的动作(在另一个切片中或使用create action助手)。
商店
使用该工具包也可以更轻松地创建商店。就像旧的'createStore'函数一样,'configureStore'函数使用reducer创建一个存储,但是默认情况下它会连接到有用的中间件中。中间件是Redux存储可以扩展为集中处理流程的方式。默认情况下,除了工具箱外,您什么都没有:
-
Redux Thunk-允许我们将函数提交为异步操作的操作
-
Redux Dev Tools-启用Redux Dev Tools浏览器扩展
-
不变性-确保状态转换始终不变
-
可序列化不变性-确保操作和状态始终可序列化。
底部3仅在调试模式下自动启用。
const store = configureStore({
reducer
});
可测性
到目前为止,我已经能够开发应用程序的核心逻辑,而无需非常关注视图。关注点的分离非常有力。由于reducer函数是纯函数,因此它也方便了测试,我们只需要指定输入和输出即可。
it(`should handle previous operation when new operation pressed`, () => {
const result = target({
value: '10',
previousValue: "12",
operation: '-'
}, keyPressed('+'));
expect(result.previousValue).toEqual(undefined);
expect(result.value).toEqual('2');
expect(result.operation).toEqual('+');
})
it(`should replace op when op present but no previous value`, () => {
const result = target({
value: '10',
previousValue: undefined,
operation: '-'
}, keyPressed('+'));
expect(result.previousValue).toEqual(undefined);
expect(result.value).toEqual('10');
expect(result.operation).toEqual('+');
});
利用我们通过调用" it"(或" test")函数定义测试的事实,我们可以使用forEach调用轻松编写参数化测试,
[
{value: '0', key: '0', expected: '0', name: 'Enter 0 with zero already present'},
{value: '0', key: '4', expected: '4', name: 'Enter 4 with zero already present'},
{value: '', key: '1', expected: '1', name: 'Enter 1'},
{value: '1', key: '2', expected: '12', name: 'Second digit'},
{value: '0', key: '.', expected: '0.', name: 'Entering . on zero'},
// ...
].forEach(({value, key, expected, name}) =>
it(`should handle key presses correctly - ${name}`, () => {
// ...
})
);
同样,所有这些在TypeScript中都更好,因为测试数据对象可以是任何形状,但是编译器仍然知道类型,并将为我们提供错误检查,自动完成,类型检查,重命名重构等。
注意,在Jest中,它和测试具有内置的参数化形式,但是对于复杂的测试数据,使用forEach并不是一个好方法。
连接到React
既然我们已经涵盖了构建状态和域逻辑的内容,那么让我们深入了解一下。该工具包无助于将状态连接到react视图,但是核心react-redux库本身已经演化为支持Redux Hooks。这使得使用useSelector挂钩函数将状态的任何部分连接到组件变得容易。
export const Display = () => {
const value = useSelector((state: State) => state.value);
const operator = useSelector((state: State) => state.operation);
return (
<div className='row'>
<div className='col-1'>{operator}</div>
<div className={`col-5 border text-right ${styles.display}`}>
{value}
</div>
</div>
);
}
有了useSelector
钩子,不仅在渲染时使用了此状态,而且还设置了此组件,因此如果此状态发生更改,它将自动重新渲染。请注意,不是任何状态都发生变化,而只是提取的状态。
同样,使用useDispatch钩子可以轻松分派任何操作
const Button: FC<ButtonProps> = ({value}) => {
const dispatch = useDispatch();
return <button onClick={() => dispatch(keyPressed(value.content))}>
{value.content}
</button>;
}
还要注意,此组件是强类型的。React也具有强大的TypeScript支持,我们可以使用" FC"键入功能组件并添加可选的props类型,在这种情况下为ButtonProps,
interface ButtonProps {
value: ButtonDescriptor;
}
TypeScript将确保道具的类型正确,易于分解,并且我们的组件在JSX中正确使用。
结论
我希望本文能为您提供一些在现代环境中结合使用React和Redux的见解,并利用TypeScript和Redux Toolkit来提高安全性并减少样板。