Markdown 如何管理页面上的数据


theme: smartblue

前言

上一篇 # Electron中如何使用SQLite存储数据,带大家学习了如何使用 SQLite 持久存储数据,然而不是所有数据都是需要 nodejs 来处理的。接下来,我们要继续完善这个笔记本应用,学习如何管理页面组件之间的数据。

添砖加瓦 —— 数据/状态管理

现在,页面上有笔记、笔记、笔记数据,如何在各个组件间传递数据呢?

Vue 的同学,通常会选择使用Vuex。而使用 React 的同学一般来说会选择使用 Redux, 有的可能会用 MobxRxjs 等。今天我想给大家介绍的是 Redux Toolkit

以往只用 Redux 确实是一个比较头疼的,配置极为麻烦。Redux Toolkit 很好的解决了这个问题,它减少了很多样板代码,它对 Typescript 的支持也很完善。除此之外,Redux Toolkit 还包括一个强大的数据获取和缓存功能,我们称之为“RTK 查询(RTK Query)”。由于我们这个应用暂时不要服务端,所以就不讲 RTK Query了。

安装 Redux Toolkit

yarn add @reduxjs/toolkit react-redux

createStore

和我们以前使用 Redux 一样,首先得创建一个 store

// src/renderer/store.ts
import { configureStore } from "@reduxjs/toolkit";
import notebooksReducer from "./reducers/notebookSlice";

export const store = configureStore({
  reducer: {
    notebooks: notebooksReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

上面我已经添加了一个名为 notebook 的 SliceSlice 直接从英文理解就是一个数据切片,在 Vuex 里这个概念叫 module,我的理解是相当于一个子数据仓库,把一个大的状态树分割成更小的枝丫来管理。

store => component

创建完 store,需要把它传递给页面组件,这里依旧是用的 react-redux 提供的 Provider

image.png

// src/renderer/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import { Editor } from "./views/Editor";
import { store } from "./store";
import { Provider } from "react-redux";
import "./global.less";
import "../static/font/iconfont.css";

const App = () => {
  return <Editor />;
};

ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

createSlice

// src/renderer/reducers/notebooksSlice.ts
import { Action, createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface NotebookOutput {
  id: number;
  name: string;
  create_at: string;
  update_at: string;
}

export interface NotebooksSliceState {
  current: NotebookOutput | null;
  list: NotebookOutput[];
}

const initialState: NotebooksSliceState = {
  current: null,
  list: [],
};

export const notebooksSlice = createSlice({
  name: "notebook",
  initialState,
  reducers: {
    setCurrent: (state, action: PayloadAction<NotebookOutput>) => {
      state.current = action.payload;
    },
    setNotebooks: (state, action: PayloadAction<NotebookOutput[]>) => {
      state.list = action.payload;
    },
  },
});

export const { setCurrent, setNotebooks } = notebooksSlice.actions;

export default notebooksSlice.reducer;

接下来,看一下如何在组件中使用

// src/renderer/views/Editor/components/Notebooks/index.tsx
import { MoreOutlined, PlusOutlined } from "@ant-design/icons";
import { Button, Dropdown, Modal } from "antd";
import cls from "classnames";
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  NotebookOutput,
  setCurrent,
  setNotebooks,
} from "../../../../reducers/notebooksSlice";
import { AppDispatch, RootState } from "../../../../store";
import {
  CreateNotebookModal,
  CreateNotebookModalRef,
} from "../CreateNotebookModal";
import "./style.less";

export const Notebooks = () => {
  const notebooks = useSelector((state: RootState) => state.notebooks);
  const dispatch = useDispatch<AppDispatch>();
  const createModalRef = useRef<CreateNotebookModalRef | null>(null);

  const handleCreateNotebook = async (name: string) => {
    await window.Bridge?.createNotebook(name);
    const data = await window.Bridge?.getNotebooks();
    if (data) {
      dispatch(setNotebooks(data));
    }
  };

  const handleSelectNotebook = (data: NotebookOutput) => {
    dispatch(setCurrent(data));
  };

  const handleDeleteNoteBook = async (data: NotebookOutput) => {
    Modal.confirm({
      title: "注意",
      content: `是否删除该笔记本 ${data.name}`,
      okText: "确认",
      cancelText: "取消",
      onOk: async () => {
        try {
          await window.Bridge?.deleteNotebook(data.id);
          const arr = notebooks.list.slice();
          const idx = arr.findIndex((n) => n.id == data.id);
          if (idx > -1) {
            arr.splice(idx, 1);
            dispatch(setNotebooks(arr));
          }
        } catch (error) {
          console.error(error);
        }
      },
    });
  };

  useEffect(() => {
    void (async () => {
      const data = await window.Bridge?.getNotebooks();
      if (data) {
        dispatch(setNotebooks(data));
      }
    })();
  }, []);

  return (
    <div className="notebooks">
      <div className="notebooks_header">
        <Button
          className="create-notebook-btn"
          icon={<PlusOutlined />}
          type="primary"
          autoFocus={false}
          onClick={() => createModalRef.current?.setVisible(true)}
        >
          新建
        </Button>

        <CreateNotebookModal
          ref={createModalRef}
          onCreateNotebook={handleCreateNotebook}
        />
      </div>
      <div className="notebooks-list">
        {notebooks.list.map((n) => {
          const c = cls("notebook-item", {
            selected: notebooks.current?.id == n.id,
          });
          return (
            <div
              className={c}
              key={n.id}
              data-id={n.id}
              onClick={() => handleSelectNotebook(n)}
            >
              <span>{n.name}</span>
              <Dropdown
                menu={{
                  items: [
                    {
                      key: "delete",
                      danger: true,
                      label: "删除笔记本",
                      onClick: () => handleDeleteNoteBook(n),
                    },
                  ],
                }}
              >
                <MoreOutlined />
              </Dropdown>
            </div>
          );
        })}
      </div>
    </div>
  );
};

主要看一下图中这一块的代码,其他地方都是类似的。这里涵盖了如何从 RootState 中获取需要的数据,然后又是如何去更新数据的。

image.png

createAsyncThunk

之前的 reducer 函数都是纯函数,那么以前我们写的有副作用的函数该如何在这里面写?
我们把之前获取笔记本列表的这部分逻辑,改成一个 asyncThunk 函数

export const fetchNotebooks = createAsyncThunk(
  "notebooks/fetchNotebooks",
  async () => {
    return ((await window.Bridge?.getNotebooks()) ?? []) as NotebookOutput[];
  }
);

fetchNotebooks 运行的时候会产生3个 action:

  • pending: notebooks/fetchNotebooks/pending
  • fulfilled: notebooks/fetchNotebooks/fulfilled
  • rejected: notebooks/fetchNotebooks/rejected

然后,我们在 notebooksSlice 中再添加一个 extraReducers 属性来处理这些 action

export const notebooksSlice = createSlice({
  name: "notebook",
  initialState,
  reducers: {
    setCurrent: (state, action: PayloadAction<NotebookOutput>) => {
      state.current = action.payload;
    },
    setNotebooks: (state, action: PayloadAction<NotebookOutput[]>) => {
      state.list = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder.addCase(fetchNotebooks.fulfilled, (state, action) => {
      if (action.payload) {
        state.list = action.payload;
      }
    });
  },
});

image.png

这里我只处理了 fulfilled 的 action,一般来说就够用了,如果你想把交互做的更细一些,可以对可能发生的错误再做 toast 提醒等。

然后,我们把组件那边的代码简化一下,对比下前后差异。

image.png

image.png

看,是不是少写了很多重复代码?

总结

最后看一下整体的交互,改完之后数据没有问题。

redux-toolkit.gif

这一篇主要是带大家学习了一个新的基于 redux 的状态管理工具,演示了常见的几个方法,想要深入学习还是需要去研读官方文档的。

项目代码 Github

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容