25、Redux 中的动作创建与应用

Redux 中的动作创建与应用

1. 动作调度基础

在 Redux 中调度动作,只需将 exampleUse 文件导入主应用文件,应用启动时它就会运行。以下是 src/index.js 需要做的小修改:

import React from 'react';
import { render } from 'react-dom';
import { App } from './containers/App';
import { Home, SinglePost, Login, NotFound, Profile } from './containers';
import { Router, Route } from './components/router';
import { history } from './history';
import { firebase } from './backend';
import configureStore from './store/configureStore';
import initialReduxState from './constants/initialState';
import './store/exampleUse';
//...

当以开发模式(使用 npm run dev )加载应用时,Redux 开发工具图标会启用。应用运行时,导入的文件会多次调用存储调度器,将动作发送到存储。不过目前还没有为动作设置处理(通过 reducers),也未与 React 连接,所以不会有有意义的变化。但打开开发者工具查看动作历史记录,会看到每个加载动作都被调度并记录。

2. 异步动作与中间件

目前在 Redux 中只能调度同步动作,但实际应用中很多情况需要基于异步动作进行更改,如网络请求、读取浏览器本地存储值等。Redux 本身不支持异步动作,因为它期望动作只是普通对象。不过可以通过集成 redux-thunk 库来启用异步动作。

redux-thunk 是一个 Redux 中间件库,它就像 Redux 的一种“过路”或传递机制。Redux 中间件是“在调度动作和该动作到达 reducer 之间的第三方扩展点”,意味着在动作被 reducer 处理之前,有机会对其进行操作。

以下是将 redux-thunk 中间件集成到应用的代码:

import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import rootReducer from '../reducers/root';
let store;
export default (initialState) => {
  if (store) {
    return store;
  }
  const createdStore = createStore(rootReducer, initialState, compose(
    applyMiddleware(
      thunk,
    ),
    window.devToolsExtension()
    )
  );
  store = createdStore;
  return store;
};

安装 redux-thunk 后就可以创建异步动作创建器。虽然在进行网络请求等异步操作,但创建的动作本身不是异步任务。 redux-thunk 让 Redux 存储在接收到 Promise 时进行评估,通过 Promise 的执行过程来调度动作。动作仍然是同步的,但 Redux 现在知道在将 Promise 传入调度函数时等待其解析。

3. 创建动作创建器
3.1 错误动作

首先创建一个错误动作,用于在出现问题时向用户显示错误信息:

import * as types from '../constants/types';
export function createError(error, info) {
    return {
        type: types.app.ERROR,
        error,
        info
    };
}
3.2 评论动作

接下来创建与评论相关的动作,包括显示、切换、更新可用评论和创建新评论:

import * as types from '../constants/types';
import * as API from '../shared/http';
import { createError } from './error';
export function showComments(postId) {
    return {
        type: types.comments.SHOW,
        postId
    };
}
export function toggleComments(postId) {
    return {
        type: types.comments.TOGGLE,
        postId
    };
}
export function updateAvailableComments(comments) {
    return {
        type: types.comments.GET,
        comments
    };
}
export function createComment(payload) {
    return dispatch => {
        return API.createComment(payload)
            .then(res => res.json())
            .then(comment => {
                dispatch({
                    type: types.comments.CREATE,
                    comment
                });
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function getCommentsForPost(postId) {
    return dispatch => {
        return API.fetchCommentsForPost(postId)
            .then(res => res.json())
            .then(comments => dispatch(updateAvailableComments(comments)))
            .catch(err => dispatch(createError(err)));
    };
}
3.3 文章动作

文章动作与评论动作类似,还包括点赞、取消点赞、创建文章、获取文章列表和加载单篇文章等功能:

import parseLinkHeader from 'parse-link-header';
import * as types from '../constants/types';
import * as API from '../shared/http';
import { createError } from './error';
import { getCommentsForPost } from './comments';
export function updateAvailablePosts(posts) {
    return {
        type: types.posts.GET,
        posts
    };
}
export function updatePaginationLinks(links) {
    return {
        type: types.posts.UPDATE_LINKS,
        links
    };
}
export function like(postId) {
    return (dispatch, getState) => {
        const { user } = getState();
        return API.likePost(postId, user.id)
            .then(res => res.json())
            .then(post => {
                dispatch({
                    type: types.posts.LIKE,
                    post
                });
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function unlike(postId) {
    return (dispatch, getState) => {
        const { user } = getState();
        return API.unlikePost(postId, user.id)
            .then(res => res.json())
            .then(post => {
                dispatch({
                    type: types.posts.UNLIKE,
                    post
                });
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function createNewPost(post) {
    return (dispatch, getState) => {
        const { user } = getState();
        post.userId = user.id;
        return API.createPost(post)
            .then(res => res.json())
            .then(newPost => {
                dispatch({
                    type: types.posts.CREATE,
                    post: newPost
                });
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function getPostsForPage(page = 'first') {
    return (dispatch, getState) => {
        const { pagination } = getState();
        const endpoint = pagination[page];
        return API.fetchPosts(endpoint)
            .then(res => {
                const links = parseLinkHeader(res.headers.get('Link'));
                return res.json().then(posts => {
                    dispatch(updatePaginationLinks(links));
                    dispatch(updateAvailablePosts(posts));
                });
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function loadPost(postId) {
    return dispatch => {
        return API.fetchPost(postId)
            .then(res => res.json())
            .then(post => {
                dispatch(updateAvailablePosts([post]));
                dispatch(getCommentsForPost(postId));
            })
            .catch(err => dispatch(createError(err)));
    };
}
4. 是否使用 Redux

完成上述动作创建器后,已经创建了创建文章和评论的初始功能,但还缺少用户认证部分。在决定是否将某些状态或功能放入 Redux 存储时,可以考虑以下两个问题:
- 是否有很多其他部分需要了解该状态或功能 :如果应用的其他部分需要知道某个状态,那么该状态可能应该放入 Redux 存储。例如,如果应用需要控制下拉菜单的打开或关闭并对其做出响应,这些状态变化应该通过存储处理;如果不需要,将状态保留在组件本地即可。
- 该状态在 Redux 中是否会更简单或更好表达 :如果只是为了使用 Redux 而将组件的状态和动作转换到 Redux 中,可能会引入额外的复杂性。但如果状态复杂,Redux 能使其更易于处理,那么可以将其包含在存储中。

考虑到用户认证逻辑,应用的其他部分需要知道用户信息,并且将其集中在存储中可以避免在不同页面重复逻辑,所以目前将用户和认证逻辑集成到 Redux 是有意义的。

以下是创建与用户相关的动作的代码:

import * as types from '../constants/types';
import { history } from '../history';
import { createError } from './error';
import { loading, loaded } from './loading';
import { getFirebaseUser, loginWithGithub, logUserOut, getFirebaseToken } from '../backend/auth';
export function loginSuccess(user, token) {
    return {
        type: types.auth.LOGIN_SUCCESS,
        user,
        token
    };
}
export function logoutSuccess() {
    return {
        type: types.auth.LOGOUT_SUCCESS
    };
}
export function logout() {
    return dispatch => {
        return logUserOut()
            .then(() => {
                history.push('/login');
                dispatch(logoutSuccess());
                window.Raven.setUserContext();
            })
            .catch(err => dispatch(createError(err)));
    };
}
export function login() {
    return dispatch => {
        return loginWithGithub().then(async () => {
            try {
                dispatch(loading());
                const user = await getFirebaseUser();
                const token = await getFirebaseToken();
                const res = await API.loadUser(user.uid);
                if (res.status === 404) {
                    const userPayload = {
                        name: user.displayName,
                        profilePicture: user.photoURL,
                        id: user.uid
                    };
                    const newUser = await API.createUser(userPayload).then(res => res.json());
                    dispatch(loginSuccess(newUser, token));
                    dispatch(loaded());
                    history.push('/');
                    return newUser;
                }
                const existingUser = await res.json();
                dispatch(loginSuccess(existingUser, token));
                dispatch(loaded());
                history.push('/');
                return existingUser;
            } catch (err) {
                createError(err);
            }
        });
    };
}

通过以上步骤,我们可以在 Redux 中创建和调度各种同步和异步动作,同时合理地决定哪些状态和功能应该放入 Redux 存储,从而构建一个健壮的应用架构。

Redux 中的动作创建与应用(续)

5. 动作调度流程总结

为了更清晰地理解 Redux 中动作的调度流程,我们可以通过以下 mermaid 流程图来展示:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([事件触发]):::startend --> B(动作创建器):::process
    B --> C(动作):::process
    C --> D(调度器):::process
    D --> E{是否为异步动作}:::decision
    E -->|是| F(中间件处理):::process
    E -->|否| G(直接传递):::process
    F --> G
    G --> H(存储):::process
    H --> I(Reducer):::process
    I --> J(新状态):::process
    J --> K(视图更新):::process

从流程图中可以看出,事件触发动作创建器生成动作,动作通过调度器传递。如果是异步动作,会先经过中间件处理,如 redux-thunk 中间件会处理 Promise 并在合适的时机调度动作。最终动作到达存储,由 Reducer 处理生成新状态,进而更新视图。

6. 不同类型动作的使用场景分析
动作类型 使用场景 示例代码
同步动作 简单的状态更新,如显示或隐藏组件、切换开关等
export function showComments(postId) {
    return {
        type: types.comments.SHOW,
        postId
    };
}

| 异步动作 | 涉及网络请求、文件读取等异步操作的场景,如获取文章列表、创建新文章等 |

export function getPostsForPage(page = 'first') {
    return (dispatch, getState) => {
        const { pagination } = getState();
        const endpoint = pagination[page];
        return API.fetchPosts(endpoint)
           .then(res => {
                const links = parseLinkHeader(res.headers.get('Link'));
                return res.json().then(posts => {
                    dispatch(updatePaginationLinks(links));
                    dispatch(updateAvailablePosts(posts));
                });
            })
           .catch(err => dispatch(createError(err)));
    };
}

| 错误处理动作 | 处理应用中出现的错误,如网络请求失败、数据解析错误等 |

export function createError(error, info) {
    return {
        type: types.app.ERROR,
        error,
        info
    };
}
7. 中间件的作用及原理深入理解

中间件在 Redux 中起着至关重要的作用。以 redux-thunk 为例,它允许我们在动作创建器中返回一个函数而不是普通对象,从而实现异步操作。其工作原理如下:
1. 拦截动作调度 :当动作被调度时, redux-thunk 中间件会拦截该动作。
2. 判断动作类型 :如果动作是一个函数,中间件会执行该函数,并将 dispatch getState 方法作为参数传递给它。
3. 处理异步操作 :在函数内部,我们可以进行异步操作,如网络请求。当异步操作完成后,通过 dispatch 方法调度其他动作。

以下是一个简单的示例,展示 redux-thunk 如何处理异步动作:

import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';

// 模拟 reducer
const reducer = (state = {}, action) => {
    switch (action.type) {
        case 'FETCH_DATA_SUCCESS':
            return { ...state, data: action.payload };
        default:
            return state;
    }
};

// 创建存储并应用中间件
const store = createStore(reducer, applyMiddleware(thunk));

// 异步动作创建器
const fetchData = () => {
    return dispatch => {
        // 模拟异步请求
        setTimeout(() => {
            const data = { message: 'Data fetched successfully' };
            dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
        }, 1000);
    };
};

// 调度异步动作
store.dispatch(fetchData());

在这个示例中, fetchData 是一个异步动作创建器,它返回一个函数。 redux-thunk 中间件会执行这个函数,并在异步操作完成后调度 FETCH_DATA_SUCCESS 动作。

8. 合理使用 Redux 的最佳实践
  • 状态划分 :根据前面提到的两个问题,合理划分哪些状态应该放入 Redux 存储,哪些应该保留在组件本地。例如,表单输入的临时值可以保留在组件本地,而用户的认证信息、文章列表等需要在多个组件中共享的状态应该放入 Redux 存储。
  • 动作命名规范 :为动作创建器和动作类型使用清晰、有意义的命名,便于代码的维护和理解。例如,使用 showComments 而不是 toggleShowHideComments 这样模糊的命名。
  • 中间件管理 :在使用中间件时,要注意中间件的顺序和功能。不同的中间件可能会有不同的执行顺序要求,确保中间件之间不会相互冲突。例如, redux-thunk 通常是第一个应用的中间件。
  • 错误处理 :在每个可能出现错误的地方都要进行错误处理,如网络请求失败、数据解析错误等。使用统一的错误动作创建器,如 createError ,将错误信息传递到存储,便于统一管理和显示错误信息。
9. 总结与展望

通过本文,我们详细介绍了 Redux 中动作的创建、调度以及异步动作的处理方法。我们学习了如何使用 redux-thunk 中间件实现异步动作,创建了各种类型的动作创建器,包括错误处理动作、评论动作、文章动作和用户相关动作。同时,我们还探讨了如何合理地决定哪些状态和功能应该放入 Redux 存储。

在未来的开发中,随着应用的不断扩展,我们可能会遇到更复杂的异步操作和状态管理需求。可以考虑使用其他 Redux 中间件库,如 redux-saga ,它可以更优雅地处理异步操作和副作用。此外,还可以结合 React Router 等库,实现更复杂的路由和状态管理。总之,合理使用 Redux 可以帮助我们构建更健壮、可维护的应用架构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值