目录
入门
我们应该什么时候使用?
Redux库和工具
Redux Toolkit
Redux DevTools 扩展
demo练习准备工作:
基础示例
Redux Toolkit示例
Redux术语和概念
不可变性Immutability
术语
Redux步骤分解
练习的demo
数据流基础-同步操作方式
异步逻辑与数据请求示例
Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
Redux 是一个小型的独立 JS 库。 但是,它通常与其他几个包一起使用:
React-Redux
Redux可以与任何UI框架集成,最常用于React。React-Redux是我们的官方软件包,它允许您的React组件通过读取状态片段和调度操作来更新存储,从而与Redux存储交互。
Redux Toolkit是我们推荐的编写 Redux 逻辑的方法。 它包含我们认为对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 构建在我们建议的最佳实践中,简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。
Redux DevTools扩展程序可以显示 Redux 存储中状态随时间变化的历史记录。这允许您有效地调试应用程序,包括使用强大的技术,如“时间旅行调试”。
安装Redux Toolkit
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
创建一个React Redux应用,使用 官方 Redux+JS 模版 它基于Create React App,它利用了Redux Toolkit和Redux与React组件的集成。
npx create-react-app my-app --template redux
安装Redux
# NPM
npm install redux
# Yarn
yarn add redux
配套工具
npm install react-redux
npm install --save-dev redux-devtools
应用的整体全局状态以对象树的方式存放于单个 store。 唯一改变状态树(state tree)的方法是创建 action,一个描述发生了什么的对象,并将其 dispatch 给 store。要指定状态树如何响应 action 来进行更新,你可以编写纯 reducer 函数,这些函数根据旧 state 和 action 来计算新 state。
import { createStore } from 'redux'
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
case 'counter/decremented':
return { value: state.value - 1 }
default:
return state
}
}
let store = createStore(counterReducer)
store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}
Redux Toolkit 简化了编写 Redux 逻辑和设置 store 的过程。 使用 Redux Toolkit,相同的逻辑如下所示:
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
incremented: state => {
state.value += 1
}
}
})
export const { incremented, decremented } = counterSlice.actions
const store = configureStore({
reducer: counterSlice.reducer
})
// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))
store.dispatch(incremented())
// {value: 1}
redux是单向数据流
"Mutable" 意为 "可改变的",而 "immutable 意为永不可改变。
JavaScript 的对象(object)和数组(array)默认都是 mutable 的。如果我创建一个对象,我可以更改其字段的内容。如果我创建一个数组,我也可以更改内容:
const obj = { a: 1, b: 2 }
// 对外仍然还是那个对象,但它的内容已经变了
obj.b = 3
const arr = ['a', 'b']
// 同样的,数组的内容改变了
arr.push('c')
arr[1] = 'd'
这就是 改变 对象或数组的例子。内存中还是原来对象或数组的引用,但里面的内容变化了。
如果想要不可变的方式来更新,代码必需先复制原来的 object/array,然后更新它的复制体。
可以使用如下或其它方式
const obj1 = { name: '冰墩墩', info: {age: 18} };
// 方式1通过扩展运算符
const obj2 = {
...obj1, // obj1的备份
info: {
...obj1.info, // ojb1.info的备份
age: 16, // 覆盖age
}
}
// 方式2通过Object.assign
const obj3 = Object.assign({}, obj1, {name: '冰墩墩', info: {age: 22}});
Action是一个具有 type
字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.
type
字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"
。我们通常把那个类型的字符串写成“域/事件名称”,其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。
action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为 payload
的字段中。
一个典型的 action 对象可能如下所示:
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
Reducer
reducer 是一个函数,接收当前的 state
和一个 action
对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState
。
Reducer 必需符合以下规则:
state
和 action
参数计算新的状态值state
。必须通过复制现有的 state
并对复制的值进行更改的方式来做 不可变更新(immutable updates)。例
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === 'counter/increment') {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1
}
}
// 返回原来的 state 不变
return state
}
store
当前 Redux 应用的状态存在于一个名为 store 的对象中。
store 是通过传入一个 reducer 来创建的,并且有一个名为 getState
的方法,它返回当前状态值:实际使用我们会通过useSelector来获取store
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
Dispatch
Redux store 有一个方法叫 dispatch
。更新 state 的唯一方法是调用 store.dispatch()
并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
dispatch 一个 action 可以形象的理解为 "触发一个事件"。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
state
dispatch({type: 'counter/increment'})
state
和当前的 action
再次运行 reducer 函数,并将返回值保存为新的 state
根据上面的学习,练习了以下简单demo,里面写的详细注释有点过于啰嗦,仅用于自己后面复盘.
demo相关有:展示贴子列表,查看贴子详情,添加贴子,编辑贴子,显示作者,下面只列举了关键几个文件,有的子组件没有贴过来,均是简单展示组件。
项目的index.js文件
涉及以下三个知识点
1.ReactDOM.render(template,targetDOM),该方法接收两个参数:第一个是创建的模板,多个dom元素外层需使用一个标签进行包裹,如 2.使用Provider, 为了让像 我们在这里引用来自 3.React Router v6路由配置,详情查看 代码实现ReactDOM.render(
来告诉 React 开始渲染我们的根
组件。
useSelector
这样的 hooks 正常工作,我们需要使用一个名为
的组件在幕后传递 Redux store,以便他们可以访问它。app/store.js
中创建的 store。然后,用
包裹整个
,并传入 store:
现在,任何调用 useSelector
或 useDispatch
的 React 组件都可以访问
中的 store。import React from 'react';
import ReactDOM from 'react-dom';
import { store } from './app/store';
import { Provider } from 'react-redux';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './App';
import { PostsList } from './features/posts/PostsList';
import { SinglePostPage } from './features/posts/SinglePostPage';
import { EditPostForm } from './features/posts/EditPostForm';
import * as serviceWorker from './serviceWorker';
import './index.css';
ReactDOM.render(
app/store.js文件
创建 Redux store 实例,导入 postsReducer等
函数,并更新对 configureStore
的调用,以便将 postsReducer
作为名为 posts
的 reducer 字段传递。
这告诉 Redux 我们希望我们的根部 state 对象内部有一个名为 posts
的字段,并且 state.posts
的所有数据都将在 dispatch action 时由 postsReducer
函数更新。
代码实现
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import postsSlice from '../features/posts/postsSlice';
import usersSlice from '../features/users/usersSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
posts: postsSlice,
users: usersSlice,
},
});
// Redux state 由 reducer 函数来更新:
// Reducers 总是通过复制现有状态值,并更新副本来不可变地生成新状态!!
// Redux Toolkit createSlice 函数为您生成“slice reducer”函数,并让您编写“mutable 可变”代码,内部自动将其转变为安全的不可变更新例:postsSlice.js
// 这些切片化 reducer 函数被添加到 configureStore 中的 reducer 字段中,并定义了 Redux store 中的数据和状态字段名称
// React 组件使用 useSelector 钩子从 store 读取数据
// 选择器函数接收整个 state 对象,并且返回需要的部分数据
// 每当 Redux store 更新时,选择器将重新运行,如果它们返回的数据发生更改,则组件将重新渲染
// React 组件使用 useDispatch 钩子 dispatch action 来更新 store
// createSlice 将为我们添加到切片的每个 reducer 函数生成 action creator 函数
// 在组件中调用 dispatch(someActionCreator()) 来 dispatch action
// Reducers 将运行,检查此 action 是否相关,并在适当时返回新状态
// 表单输入值等临时数据应保留为 React 组件状态。当用户完成表单时,dispatch 一个 Redux action 来更新 store。
postsSlice.js文件
里面涉及的知识点全部注释在相关代码段处
代码实现
import { createSlice } from '@reduxjs/toolkit';
import { nanoid } from '@reduxjs/toolkit';
const initialState = [
{ id: 1, title: 'First Post', content: 'Hello', user: '1', reactions: { thumbsUp: 1, hooray:1, heart:1,rocket:1,eyes:0} },
{ id: 2, title: 'Second Post', content: 'More text', reactions: { thumbsUp: 1, hooray:1, heart:1,rocket:1,eyes:0} },
]
// 添加状态切片
// Redux Toolkit createSlice 函数生成“slice reducer”函数
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: { // 编写 reducer 函数
// postAdded(state, action) {
// // 请记住:reducer 函数必须总是通过复制来不可变地创建新的状态值!
// // 这里调用诸如 Array.push() 之类的变异函数或修改 createSlice()
// // 中的诸如 state.someField = someValue 之类的对象字段之所以是安全的,
// // 是因为它在内部使用 Immer 库将这些突变转换为安全的不可变更新,
// // 但不要尝试在 createSlice 之外用这种方式更改任何数据!!!
// state.push(action.payload)
// },
postAdded: {
reducer(state, action) {
state.push(action.payload);
},
prepare(title, content, userId) {
// 前提: 唯一 ID 和其他随机值应该放在 action 中,而不是在 reducer 中计算.
// 如果我们这里需要一个随机 id,但reducer里是不允许出现随机值的,那么 可以利用prepare函数
// createSlice 允许我们在编写 reducer 时定义一个 prepare 函数。 prepare 函数可以接受多个参数,生成诸如唯一 ID 之类的随机值
// ,并运行需要的任何其他同步逻辑来决定哪些值进入 action 对象。然后它应该返回一个包含 payload 字段的对象。
// (返回对象还可能包含一个 meta 字段,可用于向 action 添加额外的描述性值,以及一个 error 字段,
// 该字段应该是一个布尔值,指示此 action 是否表示某种错误。)
return {
payload: {
id: nanoid(),
title,
content,
user: userId
},
meta: "meteeeeeeexxxx"
}
}
},
// 上面 postAdded 这种写法是因为要有随机值,分为两步完成 。如果不存在随机值或者 异步,
// 那直接通过以下方式
postUpadted(state, action) {
const { id, title, content } = action.payload;
console.log(JSON.stringify(state));
const existingPost = state.find(post => post.id == id)
if (existingPost) {
existingPost.title = title;
existingPost.content = content;
}
},
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
}
}
})
export const { postAdded, postUpadted, reactionAdded } = postsSlice.actions;
// 编写 postAdded reducer 函数时,createSlice 会自动生成一个同名的 “action creator” 函数
// 我们可以导出该动作创建者并在我们的 UI 组件中使用它来在用户单击“保存帖子”时分派动作。
// 默认情况下,createSlice 生成的 action creator 希望你传入一个参数,该值将作为 action.payload 放入 action 对象中
console.log('postsSlice', postsSlice)
export default postsSlice.reducer;
// 这些切片化 reducer 函数被添加到 configureStore 中的 reducer 字段中,并定义了 Redux store 中的数据和状态字段名称,见store.js
PostsList.js 贴子列表
import React from 'react';
import { useSelector } from 'react-redux';
import { PostAuthor } from './PostAuthor';
import { ReactionButtons } from './ReactionButtons';
import { Link, Outlet, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'
export const PostsList = () => {
const posts = useSelector(state => state.posts);
// const match = useMatch('postList/sent');
// console.log('match', match);
const location = useLocation();
console.log('location', location);
const navigate = useNavigate();
console.log('navigate', navigate);
const params = useParams();
console.log('params', params);
const [searchParams, setSearchparams] = useSearchParams();
console.log('searchParams', searchParams);
const renderedPosts = posts.map(post => (
{post.title}
{post.content.substring(0,100)}
查看详情
编辑贴子
))
return (
posts
{renderedPosts}
{/* 子路由渲染位置 */}
navigate('/postList', {replace: true})}>跳转
)
}
AddPostForm.js添加贴子文件
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { nanoid } from '@reduxjs/toolkit';
import { postAdded } from './postsSlice';
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [userId, setUserId] = useState('');
const dispatch = useDispatch();
const users = useSelector(state => state.users);
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onAuthorChanged = e => setUserId(e.target.value);
const canSave = Boolean(title) && Boolean(content) && Boolean(userId);
const usersOptions = users.map(user => (
))
const handleSave = () => {
if (title && content) {
// dispatch(postAdded(
// {
// id: nanoid(),
// title,
// content,
// }
// ))
// 使用prepare调用 方式
dispatch(postAdded( title, content, userId))
setTitle('');
setContent('')
}
}
return (
添加新帖子
)
}
使用 Redux Toolkit createAsyncThunk
API 来简化异步调用
postsSlice.js关于createAsyncThunk片段
// 使用createAsyncThunk API生成thunk,当调用 dispatch(fetchPosts()) 的时候,
// fetchPosts 这个 thunk 会首先 dispatch 一个 action 类型为'posts/fetchPosts/pending':,
// 可以在reducer中通过extraReducers里[fetchPosts.pending]来监听这个状态 ,将状态更改为loading,
// 一旦Promise接口请求成功,extraReducers里的[fetchPosts.fulfilled]会接收到新的posts数组,即可更新数据
// 参数1:将用作生成的 action 类型的前缀的字符串
// 参数2:一个“payload creator”回调函数,它应该返回一个包含一些数据的 Promise,或者一个被拒绝的带有错误的 Promise
export const fetchPosts = createAsyncThunk('users/fetchPosts', async () => {
const response = await client.get('http://127.0.0.1:8888/api/posts')
console.log('responseresponse', response.data.users)
return response.data.posts
})
export const addNewPost = createAsyncThunk('posts/addNewPost', async initialPost => {
const response = await client.post('http://127.0.0.1:8888/api/posts', { post: initialPost});
return response.post
})
添加以上thunk后,可以在createSlice的extraReducers里响应没有定义的切片(也就是异步请求类型,不同状态下产生的action)
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
//...忽略不写
},
// !!有时切片的 reducer 需要响应 没有 定义到该切片的 reducers 字段中的 action。这个时候就需要使用 slice 中的 extraReducers 字段。
// 例如异步请求类型,要想捕获他们的pending或fulfilled等action类型
extraReducers: {
// 'counter/increment': (state, action) => {
// // 更新帖子内容切片的 reducer 逻辑
// }
/**上面改为下面这种:
* 更好的方法是,调用 actionCreator.toString()
* actionCreator.toString()它会返回 Redux Toolkit 生成的 action 类型的字符串。??如下使用会得到右面的结果
* 打印{ [increment]: () => {}} ==>{'counter/increment': () => {})}
*/
[increment]: (state, action) => {
// 更新帖子内容切片的 reducer 逻辑
},
/**
* 需要监听由 fetchPosts thunk dispatch 的“pending”和“fulfilled” 2个 action 类型。
* 这些 action 创建者被包含在了 fetchPosts 函数中,我们可以将它们传递给 extraReducers 来监听这些 action:
* createAsyncThunk 返回的所有 3 种 action,都可以通过返回的 Promise 来处理。
* 当请求开始时,我们将 status 枚举设置为 'loading'
* 如果请求成功,我们将 status 标记为 'succeeded',并将获取的帖子添加到 state.posts
* 如果请求失败,我们会将 status 标记为 'failed',并将错误消息保存到 state 中,需要的时候可以显示出来
*/
[fetchPosts.pending]: (state, action) => {
state.status = 'loading';
},
[fetchPosts.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.posts = state.posts.concat(action.payload);
},
[fetchPosts.rejected]: (state, action) => {
state.state = 'failed';
state.error = action.error.message;
},
[addNewPost.fulfilled]: (state, action) => {
state.posts.push(action.payload);
}
}
// 如果我们为 extraReducers 传递一个函数而不是一个对象,我们可以使用 builder 参数来添加每个 case。builder.addCase() 函数接受一个纯字符串的 action 类型,或者一个 Redux Toolkit 的 action 创建者:
// extraReducers: builder => {
// builder.addCase('counter/decrement', (state, action) => {})
// builder.addCase(increment, (state, action) => {})
// }
PostsList.js里来dispatch刚刚添加 的fetchPosts,来获取接口数据
useEffect(() => {
if (postStatus === 'idle') {
// 这里一定要是函数调用,一开始写的dispatch(fetchPosts),执行也没有报错,就是不发出请求,以为是哪里写的有问题,最后定位是这里没有调用
dispatch(fetchPosts());
}
}, [postStatus, dispatch])
上面的接口请求是本地node起的http服务,代码贴到下面
api.js
var express = require('express'); //express框架模块
var path = require('path'); //系统路径模块
var fs = require('fs'); //文件模块
var bodyParser = require('body-parser'); //对post请求的请求体进行解析模块
const { application } = require('express');
var app = express();
app.use(bodyParser.urlencoded({ extended: false })); //bodyParser.urlencoded 用来解析request中body的 urlencoded字符,只支持utf-8的编码的字符,也支持自动的解析gzip和 zlib。返回的对象是一个键值对,当extended为false的时候,键值对中的值就为'String'或'Array'形式,为true的时候,则可为任何数据类型。
// 设置允许跨域请求
app.all('*', function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*'); //访问控制允许来源:所有
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); //访问控制允许报头 X-Requested-With: xhr请求
res.header('Access-Control-Allow-Metheds', 'PUT, POST, GET, DELETE, OPTIONS'); //访问控制允许方法
res.header('X-Powered-By', 'nodejs'); //自定义头信息,表示服务端用nodejs
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
//创建get接口:获取用户列表
app.get('/api/users', function(req, res) {
//console.log(req.body); //获取请求参数
var file = path.join(__dirname, 'users.json'); //文件路径,__dirname为当前运行js文件的目录
//读取json文件
fs.readFile(file, 'utf-8', function(err, data) {
if (err) {
res.send('文件读取失败');
} else {
res.send(data);
}
});
});
//创建get接口:获取贴子列表
app.get('/api/posts', function(req, res) {
//console.log(req.body); //获取请求参数
var file = path.join(__dirname, 'posts.json'); //文件路径,__dirname为当前运行js文件的目录
//读取json文件
fs.readFile(file, 'utf-8', function(err, data) {
if (err) {
res.send('文件读取失败');
} else {
res.send(data);
}
});
});
app.post('/api/posts', function(req, res) {
var postData = "";
// 数据块接收中
req.addListener("data", function (postDataChunk) {
postData += postDataChunk;
});
// 数据接收完毕,执行回调函数
req.addListener("end", function () {
console.log('ok********', postData);
fs.readFile('posts.json', 'utf-8', function(err, data) {
const oldData = JSON.parse(data);
oldData.posts.push({
...JSON.parse(postData).post,
"reactions": { "thumbsUp": 0, "hooray": 0, "heart": 0, "rocket": 0, "eyes": 0 }
});
// 先从文件中读出数据,然后push新添加的再写入
fs.writeFile('posts.json',JSON.stringify(oldData), () => {} )
res.send({
data: null,
status: 'success'
})
});
});
})
var hostName = '127.0.0.1'; //ip
var port = 8888; //端口
app.listen(port, hostName, function() {
console.log(`服务器运行在http://${hostName}:${port}`);
});
posts.json
{
"code": 0,
"msg": "请求成功",
"posts": [
{
"id": "1",
"title": "First Post",
"content": "Hello",
"user": "1",
"reactions": {
"thumbsUp": 0,
"hooray": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
},
{
"id": "2",
"title": "Second Post",
"content": "'More text",
"user": "2",
"reactions": {
"thumbsUp": 0,
"hooray": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
},
{
"title": "我是通过接口添加的新数据",
"content": "yes",
"user": "迈克儿",
"id": "ZEB8szswCzrgQ8RzpJeOK",
"reactions": {
"thumbsUp": 0,
"hooray": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
},
{
"title": "今天 星期五",
"content": "放学别走",
"user": "1",
"id": "Pjxh4hI12jN6pmB_u2_1Q",
"reactions": {
"thumbsUp": 0,
"hooray": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
},
{
"title": "明天去公园",
"content": "玉渊潭赏樱花",
"user": "哪吒",
"id": "InRwTt-jqNcYxIi1xrikW",
"reactions": {
"thumbsUp": 0,
"hooray": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
}
}
]
}
以上内容完成了redux通过redux toolkit实现同步和异步操作store。
此文根据【Redux中文官网】文档记录笔记