redux 学习总结

  • 准备 React 开发环境
  • react 实现功能,发现 State 数据分散在各个组件。在某些场景下不易维护,
  • 使用 redux 重构,实现集中化管理 State。发现组件和 store 通信麻烦,存在重复样板代码
  • 使用 react-redux 重构,优化代码
  • 使用 hooks 重构,进一步优化代码,异步处理还放在组件中
  • 使用 redux-thunk 重构,支持异步任务处理,如网络请求等。出现配置麻烦、处理可变数据、代码逻辑分散等问题
  • 使用 @reduxjs/toolkit 重构

一、准备环境

1、使用脚手架创建工程

npx create-react-app react_typescript --template typescript
复制代码

2、安装 react-router-dom

yarn add react-router-dom
复制代码

3、 安装 antd

yarn add antd

复制代码

修改 src/index.css,在文件顶部引入 antd 的样式。

@import '~antd/dist/antd.css';
复制代码

4、安装 prettier

yarn add prettier -D
复制代码
<!--.prettierrc.js-->
module.exports = {
    trailingComma: "es5", //尾随逗号
    tabWidth: 4,
    semi: true, //句尾分号
    singleQuote: false, //不允许单引号
};
复制代码

5、安装 axios

yarn add axios
复制代码

二、react 实现功能

完整代码:仓库 / react 分支

1、帖子相关

  • 帖子列表:id、title、content、user、date
  • 帖子操作:反应表情
  • 新增帖子
  • 编辑帖子
  • 帖子详情

2、用户相关

  • 用户列表:id、name

3、示例代码

以帖子列表页面为例,流程

  1. 获取数据
  2. 更新state
  3. 触发重新渲染
  4. 页面更新
/**
 * 帖子列表
 * */
const PostListPage = ({ history }: Props) => {
    const [data, setData] = useState<Post_dao[]>([]);
    useEffect(() => {
        ajax.get("/data/postList.json")
            .then((res: AxiosResponse<Post_dao[]>) => {
                console.log(res);
                const { data } = res;
                if (data) {
                    setData(data);
                }
            })
            .catch((error) => {
                message.error(error.message);
            });
    }, []);

    return (
        <div className={styles.main}>
            <Typography.Title level={3}>帖子列表</Typography.Title>
            <List
                dataSource={data}
                renderItem={(item) => (
                    <List.Item>
                        <Post
                            key={item.id}
                            data={item}
                        />
                    </List.Item>
                )}
                grid={{
                    gutter: 16,
                    column: 1,
                }}
            />
        </div>
    );
};
export default PostListPage;
复制代码

三、 redux 重构

完整代码: 仓库 / react 分支

redux 引入3个概念:store、reducer、action。流程

  1. 组件订阅 store
  2. 组件–> store–> dispatch(action)–> reducer处理–> store变更 –> store 通知组件变更–> 组件更新 State
  3. 触发组件重新渲染
  4. 页面更新
<!--action-->
export const PostListType = "postList";
export const postList_action = (data: Post_dao[]) => {
    return {
        type: PostListType,
        data,
    };
};
<!--reducer-->
const posts = (state = [], action: AnyAction) => {
    switch (action.type) {
        case PostListType: {
            return [...action.data];
        }
        default: {
            return state;
        }
    }
};

export default posts;

<!--组件-->
const PostListPage = ({ history, store }: Props) => {
    const [data, setData] = useState<Post_dao[]>([]);

    useEffect(() => {
        const updateData = () => {
            //数据绑定
            const allState = store.getState();
            const { posts } = allState;
            if (posts) {
                if (posts.length <= 0) {
                    ajax.get("/data/postList.json")
                        .then((res: AxiosResponse<Post_dao[]>) => {
                            const { data } = res;
                            if (data) {
                                store.dispatch(postList_action(data));
                            }
                        })
                        .catch((error) => {
                            message.error(error.message);
                        });
                } else {
                    setData(posts);
                }
            }
        };
        const unsubscribe = store.subscribe(updateData);
        updateData();
        return () => {
            unsubscribe();
        };
    }, [store]);

    const onSave = (post: any) => {
        store.dispatch(postNew_action(post));
    };

};
export default PostListPage;
复制代码

每个用到 store 的组件都要处理store数据映射问题:

  1. 订阅和取消订阅
  2. 从 allState 中读取到 需要的数据

四、react-redux 重构

完整代码:仓库 / react-redux 分支

react-redux 优化了 store 和 组件绑定的体验。

  • 通过 connect() 在 store 和 组件之间添加一个处理层函数mapStateToProps,把数据通过props传递给组件,优化订阅问题
  • 把从 allstate 读取数据逻辑,转移到mapStateToProps,使得组件更纯粹

4.1 实现思路

然后流程

  1. store 通过 connect()绑定组件
  2. 组件–> store–> dispatch(action)–> reducer处理–> store变更 –> mapStateToProps–>更新props–> 组件更新 State
  3. 触发组件重新渲染
  4. 页面更新
const PostListPage = (props: Props) => {
    const { history, posts, dispatch } = props;
    useEffect(() => {
        const updateData = () => {
            if (posts) {
                if (posts.length <= 0) {
                    ajax.get("/data/postList.json")
                        .then((res: AxiosResponse<Post_dao[]>) => {
                            const { data } = res;
                            if (data) {
                                dispatch(postList_action(data));
                            }
                        })
                        .catch((error) => {
                            message.error(error.message);
                        });
                }
            }
        };

        updateData();
    }, [posts, dispatch]);

};

//数据绑定
const mapStateToProps = (state: any) => {
    return { posts: state.posts };
};

export default connect(mapStateToProps)(PostListPage);
复制代码

4.2 hooks 重构

使用 react-redux 提供的 hooks 函数,可以省去 connect(),进一步优化代码

然后流程

  1. 组件–> store–> dispatch(action)–> reducer处理–> store变更 –> useSelector
  2. 组件重新渲染
  3. 页面更新
const PostListPage = (props: Props) => {
    const { history } = props;
    const dispatch = useDispatch();
    //数据绑定
    const posts: Post_dao[] = useSelector((state: any) => {
        return state.posts;
    });

    useEffect(() => {
        const updateData = () => {
            if (posts) {
                if (posts.length <= 0) {
                    ajax.get("/data/postList.json")
                        .then((res: AxiosResponse<Post_dao[]>) => {
                            const { data } = res;
                            if (data) {
                                dispatch(postList_action(data));
                            }
                        })
                        .catch((error) => {
                            message.error(error.message);
                        });
                }
            }
        };

        updateData();
    }, [posts, dispatch]);

};

export default PostListPage;
复制代码

虽然已经简化了代码,但是目前还没有支持异步任务处理。关于异常任务的中间件选择 thunks-sagas-observables,官方建议先 thunk 然后再 saga。

五、redux-thunk 重构

完整代码:仓库 / redux-thunk 分支

然后流程

  1. 组件–> store–> dispatch(异步action)–> 执行异步任务 –> dispatch(同步action)
  2. reducer处理–> store变更 –> useSelector
  3. 组件重新渲染
  4. 页面更新
<!--1.导入中间件-->
<!--milddleware.ts-->
const middlewareEnhancer = applyMiddleware(reduxThunk);
export default middlewareEnhancer;
<!--2.使用中间件-->
<!--store.ts -->
import { createStore } from "redux";
import reducer from "./reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import middlewareEnhancer from "./milddleware";
export default createStore(reducer, composeWithDevTools(middlewareEnhancer));
<!--3.添加异步action-->
export const postList_action_a = () => {
    return (dispatch: Dispatch, getState: any) => {
        const state = getState();

        if (state.posts.list && state.posts.list.length <= 0) {
            dispatch(postStatus_action(PostStatus.loading, null));

            return ajax
                .get("/data/postList.json")
                .then((res: AxiosResponse<Post_dao[]>) => {
                    setTimeout(() => {
                        dispatch(postStatus_action(PostStatus.success, null));
                        const { data } = res;
                        if (data) {
                            dispatch(postList_action(data));
                        } else {
                            dispatch(postList_action([]));
                        }
                    }, 2000);
                })
                .catch((error) => {
                    dispatch(
                        postStatus_action(PostStatus.failed, error.message)
                    );
                });
        }
    };
};

export const PostListType = "postList";
export const postList_action = (data: Post_dao[]) => {
    return {
        type: PostListType,
        data,
    };
};
<!--4.重构组件-->
const PostListPage = (props: Props) => {
    const { history } = props;
    const dispatch = useDispatch();
    const posts: Post_dao[] = useSelector((state: any) => {
        return state.posts.list;
    });
    const status: PostStatus = useSelector((state: any) => {
        return state.posts.status;
    });

    useEffect(() => {
        const updateData = () => {
            dispatch(postList_action_a());
        };

        updateData();
    }, [posts, dispatch]);


};
export default PostListPage;
复制代码

六、@reduxjs/toolkit 重构

完整代码:仓库 / @reduxjs/toolkit 分支

流程 同 redux-thunk 没有变化,主要优化了redux本身使用代码。

  • 简化了 store 配置
  • 合并 action 和 reducer 到一个 slice 文件中
  • 简化不可变数据处理
  • 自动生成大部分 action
  • reducer 自动匹配,避免 switch 语句
<!--reducer.ts-->
import posts from "../page/post/slice/postSlice";
import users from "../page/post/slice/userSlice";
export default combineReducers({ posts, users });
<!--简化了 store 配置-->
<!--store.ts-->
import reducer from "./reducer";
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({ reducer });
<!--合并 action 和 reducer-->
<!--postSlice.ts-->
export const postSlice = createSlice({
    name: "posts",
    initialState,
    reducers: {
        postStatus: (
            state: any,
            action: PayloadAction<{
                status: PostStatus;
                message: string | null;
            }>
        ) => {
            state.status = action.payload.status;
            state.message = action.payload.message;
        },
        postList: (state: any, action: PayloadAction<Post_dao[]>) => {
            <!--简化不可变数据处理-->
            state.list = action.payload;
        },

    },
});
export const postList_a = () => {
    return (dispatch: Dispatch, getState: any) => {
        const state = getState();

        if (state.posts.list && state.posts.list.length <= 0) {
            dispatch(postStatus({ status: PostStatus.loading, message: null }));

            return ajax
                .get("/data/postList.json")
                .then((res: AxiosResponse<Post_dao[]>) => {
                    setTimeout(() => {
                        dispatch(
                            postStatus({
                                status: PostStatus.success,
                                message: null,
                            })
                        );
                        const { data } = res;
                        if (data) {
                            dispatch(postList(data));
                        } else {
                            dispatch(postList([]));
                        }
                    }, 2000);
                })
                .catch((error) => {
                    dispatch(
                        postStatus({
                            status: PostStatus.failed,
                            message: error.message,
                        })
                    );
                });
        }
    };
};

<!--自动生成action-->
export const { postStatus, postList } = postSlice.actions;
<!--reducer 自动匹配-->
export default postSlice.reducer;

<!--PostListPage-->
const PostListPage = (props: Props) => {
    const { history } = props;
    const dispatch = useDispatch();
    const posts: Post_dao[] = useSelector((state: any) => {
        return state.posts.list;
    });
    const status: PostStatus = useSelector((state: any) => {
        return state.posts.status;
    });

    useEffect(() => {
        const updateData = () => {
            dispatch(postList_a());
        };

        updateData();
    }, [posts, dispatch]);

};

export default PostListPage;
复制代码

七、总结

7.1 关于收获

  1. redux 并没有“消灭”组件的局部 state,但是从概念上剥离维护 state 逻辑
  2. react-redux 通过 useSelector 或者 mapStateToProps 剥离(转移/隐藏)绑定组件逻辑
  3. redux-thunk 以中间件的方式弥补 redux 不能异步缺陷
  4. 通过引入新概念(store、reducer、action、slice、thunk)和中间环节(组件绑定、中间件),把状态维护逻辑剥离出去,与组件解耦,使组件更纯粹

7.2 关于要不要用

  1. 官方不建议无脑使用
  2. 但是还是要无脑使用

7.3 关于 @reduxjs/toolkit

  1. 用的很方便,但是不利于学习
  2. 直接隐藏了不可变数据处理,与 React 关于不可变数据处理要求“相悖”
  3. 直接屏蔽了 action 对象、生成函数,不利于 redux 3大概念理解

参考资料

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享