React Redux入门

目录

入门

我们应该什么时候使用?

Redux库和工具

Redux Toolkit

Redux DevTools 扩展

demo练习准备工作:

 基础示例

Redux Toolkit示例

Redux术语和概念

 不可变性Immutability

术语

 Redux步骤分解

练习的demo 

数据流基础-同步操作方式

异步逻辑与数据请求示例


入门

Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

我们应该什么时候使用?

  • 在应用的大量地方,都存在大量的状态
  • 应用状态会随着时间的推移而频繁更新
  • 更新该状态的逻辑可能很复杂
  • 中型和大型代码量的应用,很多人协同开发

Redux库和工具

Redux 是一个小型的独立 JS 库。 但是,它通常与其他几个包一起使用:

React-Redux

Redux可以与任何UI框架集成,最常用于React。React-Redux是我们的官方软件包,它允许您的React组件通过读取状态片段和调度操作来更新存储,从而与Redux存储交互。

Redux Toolkit

Redux Toolkit是我们推荐的编写 Redux 逻辑的方法。 它包含我们认为对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 构建在我们建议的最佳实践中,简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。

Redux DevTools 扩展

Redux DevTools扩展程序可以显示 Redux 存储中状态随时间变化的历史记录。这允许您有效地调试应用程序,包括使用强大的技术,如“时间旅行调试”。

demo练习准备工作:

安装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 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术语和概念

  • state:驱动应用的真实数据源头
  • view:基于当前状态的 UI 声明性描述
  • actions:根据用户输入在应用程序中发生的事件,并触发状态更新

 redux是单向数据流

React Redux入门_第1张图片

 不可变性Immutability

"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}

 Redux步骤分解

  • 初始启动:
    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当 UI 首次渲染时,UI 组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:
    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
    • store 通知所有订阅过的 UI,通知它们 store 发生更新
    • 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

练习的demo 

数据流基础-同步操作方式

根据上面的学习,练习了以下简单demo,里面写的详细注释有点过于啰嗦,仅用于自己后面复盘.

demo相关有:展示贴子列表,查看贴子详情,添加贴子,编辑贴子,显示作者,下面只列举了关键几个文件,有的子组件没有贴过来,均是简单展示组件。

项目的index.js文件

涉及以下三个知识点

1.ReactDOM.render(template,targetDOM),该方法接收两个参数:第一个是创建的模板,多个dom元素外层需使用一个标签进行包裹,如

;第二个参数是插入该模板的目标位置。我们总是必须调用 ReactDOM.render() 来告诉 React 开始渲染我们的根  组件。

2.使用Provider, 为了让像 useSelector 这样的 hooks 正常工作,我们需要使用一个名为  的组件在幕后传递 Redux store,以便他们可以访问它。

我们在这里引用来自 app/store.js 中创建的 store。然后,用  包裹整个 ,并传入 store:现在,任何调用 useSelector 或 useDispatch 的 React 组件都可以访问  中的 store。

3.React Router v6路由配置,详情查看

 代码实现

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(
  
    
      
        
          } />
          {/* 嵌套路由 */}
          }>
            我是index默认子路由
} /> 我是子路由元素1
} /> 我是子路由元素2
} /> } /> } /> } /> , document.getElementById('root') ); serviceWorker.unregister();

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 (
    

添加新帖子