现在,有了 Hooks, 甚至可以自己编写 Reducer,通过 Proxy 对状态进行操作。
但是,为了更融入市场而且现在有了 Redux Tookit(可以减少很多代码量),所以,这里还是用 Redux。
-
Redux 在我的理解看来就是
事件驱动
的一个典型模式,记住这点,就可以很明白为什么有action
dispatch
。 -
而 Reducer 的概念是因为使用 Proxy 代理模式,这种方式可以避免用户直接操作状态对象造成"污染"。
所以,记住以上两点,就可以很好地去使用 Redux。以及接下来要讨论的 Hooks + Redux。
开始
1. 创建 Next.js 项目
yarn create next-app --typescript
2. 安装 Redux
|
|
以上安装了 React-Redux
、TypeScript支持
、Redux工具包
(toolkit 包含了 Redux 核心 + Thunk + Reselect)
以及用于开发测试的 redux-devtools
工具。
注意:以上的安装方式是在已有的项目中安装的(Next.js 项目中)。
3. 改变目录结构
📦src
├─ 📂app
│ ├─ 📜hooks.ts
│ └─ 📜store.ts
├─ 📂features
│ └─ 📂counter
│ │ ├─ 📜Counter.module.css
│ │ ├─ 📜Counter.tsx
│ │ ├─ 📜counterAPI.ts
│ │ └─ 📜counterSlice.ts
└─ 📂pages
│ ├─ 📂api
│ │ └─ 📜hello.ts
│ ├─ 📜index.tsx
│ └─ 📜_app.tsx
将 pages 放入 新建的 src 目录中:
4. 创建 Store
因为我喜欢 TS 和 Hooks,所以创建 Store 的时候步骤会多一点,但是这是值得的。
顺带一提的是,Next.js 是我的选择,可以没有 Redux,但是不能没有 Next.js。
所以,配置的时候我会根据 Next.js 的目录做一些配置。
在src目录中新建一个app目录
|
|
|
|
|
|
5. Counter 计数器
src/features/counter/Counter.tsx
import React, { useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
const incrementValue = Number(incrementAmount) || 0;
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(incrementValue))}
>
添加金额
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
异步添加
</button>
<button
className={styles.button}
onClick={() => dispatch(incrementIfOdd(incrementValue))}
>
如果奇数则添加
</button>
</div>
</div>
);
}
src/features/counter/Counter.module.css
.row {
display: flex;
align-items: center;
justify-content: center;
}
.row > button {
margin-left: 4px;
margin-right: 8px;
}
.row:not(:last-child) {
margin-bottom: 16px;
}
.value {
font-size: 78px;
padding-left: 16px;
padding-right: 16px;
margin-top: 2px;
font-family: 'Courier New', Courier, monospace;
}
.button {
appearance: none;
background: none;
font-size: 32px;
padding-left: 12px;
padding-right: 12px;
outline: none;
border: 2px solid transparent;
color: rgb(112, 76, 182);
padding-bottom: 4px;
cursor: pointer;
background-color: rgba(112, 76, 182, 0.1);
border-radius: 2px;
transition: all 0.15s;
}
.textbox {
font-size: 32px;
padding: 2px;
width: 64px;
text-align: center;
margin-right: 4px;
}
.button:hover,
.button:focus {
border: 2px solid rgba(112, 76, 182, 0.4);
}
.button:active {
background-color: rgba(112, 76, 182, 0.2);
}
.asyncButton {
composes: button;
position: relative;
}
.asyncButton:after {
content: '';
background-color: rgba(112, 76, 182, 0.15);
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 0;
transition: width 1s linear, opacity 0.5s ease 1s;
}
.asyncButton:active:after {
width: 0%;
opacity: 1;
transition: 0s;
}
src/features/counter/counterAPI.ts
// 模拟对数据发出异步请求的模拟函数
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
src/features/counter/counterSlice.ts
|
|
src/pages/index.tsx
import { Counter } from '../features/counter/Counter'
import styles from '../../styles/Home.module.css'
const IndexPage: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Redux Toolkit</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.header}>
<img src="/vercel.svg" className={styles.logo} alt="logo" />
<Counter />
</main>
</div>
)
}
export default IndexPage