最佳 Redux 架构
引言
我想提醒一下各位,你们可能会对建筑设计发表意见,我并不完全尊重你们的意见,所以如果你们有什么意见,请在评论区留言,谢谢。
堆栈:React, NextJs, Typescript, Redux。
这篇文章的目的不是为了编写应用程序,而是为了展示Redux与TypeScript在React中的强大功能,当然,我们将使用Next.js来编写一些示例 API 请求。
那么,让我们开始吧。
第一步非常简单
npx create-next-app --typescript
接下来,我们安装 npm 依赖项。
npm i redux react-redux redux-thunk reselect
您还可以删除所有无用文件。
首先,store在根文件夹中添加一个文件夹,并在其中创建一个文件index.tsx,然后创建另一个文件夹modules,在这个文件夹中我们再创建一个文件index.ts,这里也创建一个名为 的另一个文件夹App。
所以,存储文件夹应该看起来像这样。 之后,继续创建基础模块结构:
store/modules/Appindex.ts, action.ts, enums.ts, hooks.ts, reducers.ts selectors.ts, types.ts
enum.ts(对于每个新操作,您都需要在枚举类型中创建一个新属性https://www.typescriptlang.org/docs/handbook/enums.html)
export enum TypeNames {
HANDLE_CHANGE_EXAMPLE_STATUS = 'HANDLE_CHANGE_EXAMPLE_STATUS'
}
2.接下来,为了实现这个功能,我们需要安装开发依赖项——utility-types——types.ts这是最重要的部分。
import { $Values } from 'utility-types';
import { TypeNames } from './enums';
只需导入TypeNames即可$Values
export type AppInitialStateType = {
isThisArchitecturePerfect: boolean;
};
描述哪种类型具有 AppState
export type PayloadTypes = {
[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS]: {
isThisArchitecturePerfect: boolean;
};
};
export type ActionsValueTypes = {
toChangeStatusOfExample: {
type: typeof TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_EXAMPLE_STATUS];
};
};
我们需要用这段代码来告诉 reducer 我们有哪些不同类型的 action。`specialization
*`toChangeStatusOfExample可以随意命名,但我也会给它起一个相同的名字作为(action 函数,不过现在说这个有点早)。
export type AppActionTypes = $Values<ActionsValueTypes>
在这一步,我们需要施展 TypeScript 的魔法,我们很快就会看到我所说的魔法是什么。
所以最终我们的types.ts文件应该看起来像这样
import { $Values } from 'utility-types';
import { TypeNames } from './enums';
export type PayloadTypes = {
[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
isThisArchitecturePerfect: boolean;
};
};
export type ActionsValueTypes = {
toChangeStatusOfExample: {
type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
};
};
export type AppActionTypes = $Values<ActionsValueTypes>;
export type AppInitialStateType = {
isThisArchitecturePerfect: boolean;
};
你可以认为它很臃肿,代码也过于复杂,但如果你珍惜时间,它将为你将来节省大量时间。
3.接下来要提交文件reducers.ts
import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';
首先,我们像往常一样导入模块。
const initialState: AppInitialStateType = {};
令人惊讶的是,正如你所看到的,这是 TypeScript 的神奇之处,因为我们已经赋予了initialState类型AppInitialStateType描述,即 const 应该具有属性isThisArchitecturePerfect,isThisArchitecturePerfect所以 当我们开始编写一些东西时,我们将再次看到 TypeScript 的神奇之处。
因此,当我们开始编写代码时,我们将再次见证 TypeScript 的神奇之处。
export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
switch (action.type) {
default:
return state;
}
};
Pro 临时版没什么特别的,只是基本的带有 switch 结构的 redux reducer。
- 我们
index.ts只是出口我们的建筑appReducer产品default。
import { appReducer as app } from './reducers';
export default app;
至少现在我们应该有类似的东西。
//enum.ts**
export enum TypeNames {
HANDLE_CHANGE_STATUS_OF_EXAMPLE = 'HANDLE_CHANGE_STATUS_OF_EXAMPLE',
}
//types.ts**
import { $Values } from 'utility-types';
import { TypeNames } from './enums';
export type PayloadTypes = {
[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
isThisArchitecturePerfect: boolean;
};
};
export type ActionsValueTypes = {
toChangeStatusOfExample: {
type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
};
};
export type AppActionTypes = $Values<ActionsValueTypes>;
export type AppInitialStateType = {
isThisArchitecturePerfect: boolean;
}
//reducers.ts
import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';
const initialState: AppInitialStateType = {
isThisArchitecturePerfect: true,
};
export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
switch (action.type) {
default:
return state;
}
};
//index.ts
import { appReducer as app } from './reducers';
export default app;
所以如果是的话,我表示祝贺;但如果不是全部,那么在store/modules/index.ts
export { default as app } from './App';
这是 ES6 js 的一个特性。
然后我们应该store/index.ts通过编写以下代码将其连接起来:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import * as reducers from './modules';
const combinedRedusers = combineReducers({ ...reducers });
const configureStore = createStore(combinecRedusers, compose(applyMiddleware(thunkMiddleware)));
export default configureStore;
* as reducers将会导入你在上一步导入的所有 reducer,当然,我们将其应用于thunkMiddleware异步代码。并且,当然也会导出 store。
之后,我们需要将存储连接到我们的pages/_app.tsx文件,我们可以通过以下方式实现:
layouts在文件夹中创建StoreLayout,这里创建index.tsx包含<Provider store={store}>{children}</Provider>,我得到类似这样的内容:
import { FC } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import store from './../../store';
const StoreLayout: FC = ({ children }) => {
return <ReduxProvider store={store}>{children}</ReduxProvider>;
};
export default StoreLayout;
2.layouts它的主要特点是,首先我们layouts/index.tsx用以下代码创建一个文件:
import { FC } from 'react';
export const ComposeLayouts: FC<{ layouts: any[] }> = ({ layouts, children }) => {
if (!layouts?.length) return children;
return layouts.reverse().reduce((acc: any, Layout: any) => <Layout>{acc}</Layout>, children);
};
主要思路是避免嵌套,Providers因为那样至少会有很多不同的嵌套。我们可以使用`reduce()` 函数Providers简化操作。 最后,我们需要将默认的 `next` 代码更改为我们自己的代码。pages/_app.tsx
import type { AppProps } from 'next/app';
import StoreLayout from '../layouts/StoreLayout';
import { ComposeLayouts } from '../layouts/index';
const _App = ({ Component, pageProps }: AppProps) => {
const layouts = [StoreLayout];
return (
<ComposeLayouts layouts={layouts}>
<Component {...pageProps} />
</ComposeLayouts>
);
};
export default _App;
当然,我们希望状态不是静态的,所以为了实现这一点,我们需要迁移到store/modules/App/action.ts并编写一个简单的动作函数,就像这样:
import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';
export const toChangeThemePropertyies = (
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
payload
});
重要的是要给出payload(param of function)正确的类型,由于我们有枚举类型名称,所以不会出现类型命名错误。最令人印象深刻的是,当我们编写此操作应返回AppActionTypes(其类型包含所有操作类型)时,然后在函数中编写type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,有效负载将自动找到。我们很快就会看到示例。
此外,我们还有机会store/modules/App/selectors.ts使用reselect库来访问我们的状态。其主要思想是,如果 store 发生变化,而我们又使用了 store 中的某个值,组件将重新渲染reselect,这非常强大。但在我们开始创建 reducer 之前,我们需要一些准备工作。RootStoreType我喜欢创建一个新的全局文件夹models,并在这里创建文件types.ts
:
import { AppInitialStateType } from '../store/modules/App/types';
export type RootStoreType = { app: AppInitialStateType };
在这段代码中,我们应该描述RootStoreType所有内容reducers。现在回到……store/modules/App/selectors.ts
一如既往:
import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';
然后,良好的实践是开始用 `get` 来命名你的选择器。
- `someName
,like that:export const getIsThisArchitecturePerfect= createSelector()Also,createSelector` 有 2 个参数: - 包含函数的数组(在本例中)
(state:RootStoreType) =>state.app.isThisArchitecturePerfect - 该函数接收一个参数(前一个数组的返回值),并返回你需要的值,结果代码如下:
import { RootStoreType } from '../../../models/types';
import { createSelector } from 'reselect';
export const getIsThisArchitecturePerfect= createSelector(
[(state: RootStoreType) => state.app.isThisArchitecturePerfect],
isThisArchitecturePerfect => isThisArchitecturePerfect
);
最后,我们可以测试一下我们的逻辑是否有效,为此,请移步pages/index.tsx;并编写以下代码:
import { useSelector } from 'react-redux';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';
const Index = () => {
const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
console.log(isThisArchitecturePerfect);
return <></>;
};
export default Index;
我们导入useSelector来访问我们的 store,并将我们的选择器粘贴到这里,然后console.log(isThisArchitecturePerfect)我们就能看到结果了。
保存并运行。
npm run dev
(按 F12 打开开发者工具,开玩笑啦,大家都知道)
我想你们可能会问我,为什么我们的应用这么静态,我的回答是,是的,现在,我们来添加一些动态效果。为了让应用看起来更好,我们添加一些简单的样式和 JSX 标记,还
需要使用useDispatch()来更改 store 的值,并导入 action 函数toChangeThemePropertyies。另外,我们还需要创建两个函数来更改值(第一个设置为 true,第二个设置为 false),就像这样:
正如你所见,我特别设置了'true'`not true`,这就是 TypeScript 的神奇之处,你总能确保代码按预期运行。我不使用 CSS,因为我非常喜欢使用JSS,它拥有令人难以置信的功能,我完全不明白为什么JSS并不那么流行,但这与样式无关。
import { useDispatch, useSelector } from 'react-redux';
import { toChangeThemePropertyies } from '../store/modules/App/actions';
import { getIsThisArchitecturePerfect } from '../store/modules/App/selectors';
const Index = () => {
const isThisArchitecturePerfect = useSelector(getIsThisArchitecturePerfect);
const dispatch = useDispatch();
const handleSetExampleStatusIsTrue = () => {
dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: true }));
};
const handleSetExampleStatusIsFalse = () => {
dispatch(toChangeThemePropertyies({ isThisArchitecturePerfect: false }));
};
const containerStyling = {
width: 'calc(100vw + 2px)',
margin: -10,
height: '100vh',
display: 'grid',
placeItems: 'center',
background: '#222222',
};
const textStyling = {
color: 'white',
fontFamily: 'Monospace',
};
const buttonContainerStyling = {
display: 'flex',
gap: 10,
marginTop: 20,
alignItems: 'center',
justifyContent: 'center',
};
const buttonStyling = {
...textStyling,
borderRadius: 8,
cursor: 'pointer',
border: '1px solid white',
background: 'transparent',
padding: '8px 42px',
width: '50%',
fontSize: 18,
fontFamily: 'Monospace',
};
return (
<>
<div style={containerStyling}>
<div>
<h1 style={textStyling}>{'- Is This Architecture Perfect?'}</h1>
<h1 style={textStyling}>{`- ${isThisArchitecturePerfect}`.toUpperCase()}</h1>
<div style={buttonContainerStyling}>
<button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsTrue}>
True
</button>
<button style={{ ...buttonStyling, textTransform: 'uppercase' }} onClick={handleSetExampleStatusIsFalse}>
False
</button>
</div>
</div>
</div>
</>
);
};
export default Index;
如果你足够细心,我想你应该知道为什么代码不起作用了,所以试着自己修复这个小细节。如果你不想自己动手,可以试试其他方法。
解决方法是store/modules/App/reducers.ts我们忘记写case代码了,reducer switch construction所以要解决这个问题,我们需要写这段代码。
case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: {
const { isThisArchitecturePerfect } = action.payload;
return { ...state, isThisArchitecturePerfect };
}
我有一个功能可以改进这段代码。
//if your action.payload is the same as property in initial state u can write like this:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE1:
//case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE2: ({ ...state, ...action.payload });
// if not, just create a new case
case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE: ({ ...state, ...action.payload });
所以现在一切都能正常运行,但这还不是全部,因为正如我在引言中所说,我们将编写一些简单的 API,所以打开或创建一个pages/api文件,并在其中创建一个包含你的 API 路由的文件,在我的例子中是pages/api/example,请参考官方文档。
import type { NextApiRequest, NextApiResponse } from 'next';
import { ApiExampleResType } from '../../models/types';
export default (req: NextApiRequest, res: NextApiResponse<ApiExampleResType>) => {
res.status(200).json({ title: '- Is This Architecture Perfect?' });
};
是的,还有models/types.ts写入类型
export type ApiExampleResType = { title: string };
这就是我们需要用到的“TypeScript魔法”。然后,由于Next.js的getServerSideProps方法存在一些问题,所以我们在这里会简化任务,但至少在实际应用中你应该使用Next.js的getServerSideProps方法。
所以你的任务是创建一个有效载荷类型的动作函数ApiExampleResType,仅用于训练,如果你懒得做,可以直接看结果:
//enum.ts**
HANDLE_CHANGE_TITLE_OF_EXAMPLE ='HANDLE_CHANGE_TITLE_OF_EXAMPLE',
//types.ts**
import { $Values } from 'utility-types';
import { TypeNames } from './enums';
import { ApiExampleResType } from './../../../models/types';
export type PayloadTypes = {
[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]: {
isThisArchitecturePerfect: boolean;
};
[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]: ApiExampleResType;
};
export type ActionsValueTypes = {
toChangeSphereCursorTitle: {
type: typeof TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE];
};
toChangeTitleOfExample: {
type: typeof TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE;
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE];
};
};
export type AppActionTypes = $Values<ActionsValueTypes>;
export type AppInitialStateType = {
isThisArchitecturePerfect: boolean;
} & ApiExampleResType;
//reducers.ts
import { TypeNames } from './enums';
import { AppActionTypes, AppInitialStateType } from './types';
const initialState: AppInitialStateType = {
isThisArchitecturePerfect: true,
title: 'Nothing',
};
export const appReducer = (state = initialState, action: AppActionTypes): AppInitialStateType => {
switch (action.type) {
case TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE:
case TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE:
return { ...state, ...action.payload };
default:
return state;
}
};
//action.ts
import { TypeNames } from './enums';
import { AppActionTypes, PayloadTypes } from './types';
export const toChangeThemePropertyies = (
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE]
): AppActionTypes => ({
type: TypeNames.HANDLE_CHANGE_STATUS_OF_EXAMPLE,
payload,
});
export const toChangeTitleOfExample = (
payload: PayloadTypes[TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE]
): AppActionTypes => ({
type: TypeNames.HANDLE_CHANGE_TITLE_OF_EXAMPLE,
payload,
});
您写的代码也一样(恭喜!),为了访问应用程序状态的新属性,我们需要编写一个新的选择器,下一步就是selectors.ts添加这个选择器。
export const getTitle= createSelector(
[(state: RootStoreType) => state.app.title],
title => title
);
倒数第二步,opetations.ts
首先导入所有依赖项
//types
import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { AppActionTypes } from './types';
//action
import { toChangeTitleOfExample } from './actions';
其次,我们创建了一个 thunk 函数,该函数包含一个ActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>>闭async 包,该闭包(dispatch: Dispatch<AppActionTypes>): Promise<Action> =>
会向我们发送 fetch 请求/api/example并返回结果dispatch(toChangeTitleOfExample(awaited result))。这可能有点奇怪,但结果是我们有
import { Action, ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { RootStoreType } from '../../../models/types';
import { toChangeTitleOfExample } from './actions';
import { AppActionTypes } from './types';
export const operatoToSetExampleTitle:
ActionCreator<ThunkAction<Promise<Action>, RootStoreType, void, any>> =
() =>
async (dispatch: Dispatch<AppActionTypes>): Promise<Action> => {
const result = await fetch('/api/example', { method: 'GET' });
const { title } = await result.json();
return dispatch(toChangeTitleOfExample({ title }));
};
最后一步是pages/index.tsx:
const title = useSelector(getTitle);
useEffect(() => {
dispatch(operatoToSetExampleTitle());
}, []);
在使用 Next.js 时,这不是最佳实践,但作为示例来说也不是最糟糕的,useEffect (()=>{...},[]) - 仅在挂载时运行,因此hooks.ts我们需要在有重复逻辑时使用operations.ts它reducers.ts。
结论
如果你仍然觉得它很笨重,我保证,如果你尝试使用这种结构,你会发现它非常棒,之后你将无法再使用其他架构。
感谢阅读,我非常感激♥。
文章来源:https://dev.to/pas8/best-redux-architecture-26j1

