通过 redux-toolkit 在 React 中处理具有依赖关系的 action-creatos

Processing action-creatos with dependencies in React via redux-toolkit

在 React + Redux 前端中,我目前正在尝试将 HATEOAS 集成到整个流程中。应用程序最初启动时除了可以找到后端的基本 URI 之外没有任何其他信息,后端将向前端请求的每个资源添加 URI。

应用程序本身目前能够上传一个存档文件,其中包含后端应根据前端通过 multipart/form-data 上传传递给后端的某些配置进行处理的文件。上传完成后,后端将为上传创建一个新任务并开始处理上传,这会导致计算一些值,这些值最终存储在数据库中,后端的控制器负责为处理的某些文件公开各种资源。

我目前使用 ReduxJS 工具包和切片结合异步 thunk 来管理和访问 Redux 存储。商店本身有一个专用于 return 由后端 API 编辑的部分,即全局 links 部分、页面信息等,并将存储每次调用的结果稍微重新映射。

我在这里面临的挑战之一是,在最初请求负责呈现所有任务的组件时,该组件首先需要在 link 中查找预定义的 link-关系名称后端 API 和连续调用应该重用可用信息。在这里,我想出了一个像这样的动作创建者函数:

type HalLinks = { [rel: string]: HalLink };

const requestLinks = async (uri: string, state: ApiState): Promise<[HalLinks, APILinkResponse | undefined]> => {
    let links: APILinkResponse | undefined;
    let rels: { [rel: string]: HalLink; };
    if (!state.links || Object.keys(state.links).length === 0) {
        console.debug(`Requesting links from ${uri}`);
        const linkLookup = await axios(uri, requestConfig);
        links = linkLookup.data;
        if (links) {
            rels = links._links;
        } else {
            throw new Error('Cannot resolve links in response');
        }
    } else {
        links = undefined;
        rels = state.links;
    }
    return [rels, links];
}

const lookupUriForRelName = (links: HalLinks, relName: string): HalLink | undefined => {
    if (links) {
        if (relName in links) {
            return links[relName];
        } else {
            return undefined;
        }
    } else {
        return undefined;
    }
}

const requestResource = async (link: HalLink, pageOptions?: PageOptions, filterOptions?: FilterOptions) => {
    let href: string;
    if (link.templated) {
        const templated: URITemplate = utpl(link.href);
        href = fillTemplateParameters(templated, pageOptions, filterOptions);

    } else {
        href = link.href;
    }
    console.debug(`Attempting to request URI ${href}`)

    const response = await axios.get(href, requestConfig);
    return response.data;
}

const lookup = async <T> (state: ApiState, uri: string, relName: string, pageOptions?: PageOptions, filterOptions?: FilterOptions): Promise<[T, APILinkResponse | undefined]> => {
    const [rels, links] = await requestLinks(uri, state);
    const link: HalLink | undefined = lookupUriForRelName(rels, relName);
    if (link) {
        const data = await requestResource(link, pageOptions, filterOptions);
        return [data, links];
    } else {
        throw new Error('No link relation for relation name ' + relName + ' found');
    }
}

export const requestTasksAsync = createAsyncThunk<[APITasksResponse, APILinkResponse | undefined], { uri: string; pageOptions?: PageOptions; filterOptions?: FilterOptions; }>(
    'api/tasks',
    async ({ uri, pageOptions, filterOptions }: { uri: string, pageOptions?: PageOptions, filterOptions?: FilterOptions }, { getState }) => {
        const state: ApiState = getState() as ApiState;
        const TASK_REL = 'http://acme.com/rel/tasks'; // RFC 8288 compliant link relation extension according to section 2.1.2

        return await lookup<APITasksResponse>(state, uri, TASK_REL, pageOptions, filterOptions);
    }
);

在上面的代码中,我基本上从初始 URI 标识的资源中查找 links returned,以防它们不存在于 Redux 存储或状态中,或者太老了。如果我们在跳过此步骤之前收集了 link,而是重用 state/store 中可用的信息(所有这些都发生在 requestLinks(...) 函数中)。一旦 link 可用,我们最终可以请求为我们想要获取的任务信息提供服务的资源。

上面的 action creator 的 reducer 看起来像这样:

export const apiSlice = createSlice({
    name: 'api',
    initialState,
    reducers: {
        ...
    },
    extraReducers: (builder) => {
        builder
            ...
            .addCase(requestTasksAsync.fulfilled, (state, action) => {
                const parts: [APITasksResponse, APILinkResponse | undefined] = action.payload;
                const tasks: APITasksResponse = parts[0];
                const linksResponse: APILinkResponse | undefined = parts[1];

                // update any link information received in preceeding calls ot
                // the performed actin
                if (linksResponse) {
                    processsLinks(linksResponse._links, state);
                }

                processsLinks(tasks._links, state);
                processPage(tasks.page, state);
                processTasks(tasks._embedded.tasks, state);

                state.status = StateTypes.SUCCESS;
            })
            ...
    }
});

基本上是从哪里获取和处理两个 HTTP 响应。如果我们在不必再次查找之前已经检索到这些 URI,因此 links 响应未定义,我们只需使用 if 语句将其过滤掉。

各个流程函数只是辅助函数,用于减少reducer中的代码。 IE。 processTasks 函数只是将给定 taskId 的任务添加到状态中存在的记录中:

const processTasks = (tasks: TaskType[], state: ApiState) => {
    for (let i = 0; i < tasks.length; i++) {
        let task: TaskType = tasks[i];
        state.tasks[task.taskId] = task;
    }
}

而 link 根据是使用自定义 link 关系还是由 IANA 指定的关系(即 upnext, prev, ...)

const processsLinks = (links: { [rel: string]: HalLink }, state: ApiState) => {
    if (links) {
        Object.keys(links).forEach(rel => {
            if (rel.startsWith('http')) {
                if (!state.links[rel]) {
                    console.debug(`rel ${rel} not yet present in state.links. Adding it with URI ${links[rel]}`);
                    state.links[rel] = links[rel];
                } else {
                    console.debug(`rel ${rel} already present in state with value ${state.links[rel]}. Not going to add value ${links[rel]}`);
                }
            } else {
                if (state.current) {
                    console.debug(`Updateing rel ${rel} for current component to point to URI ${links[rel]}`);
                    state.current.links[rel] = links[rel];
                } else {
                    state.current = { links: { [rel]: links[rel] } };
                    console.debug(`Created new object for current component and assigned it the link for relation ${rel} with uri ${links[rel]}`);
                }
            }
        });
    }
}

在 TaskOverview 组件中,基本上使用以下代码调度操作:

    const [pageOptions, setPageOptions] = useState<PageOptions | undefined>();
    const [filterOptions, setFilterOptions] = useState<TaskFilterOptions | undefined>()
    const state: StateTypes = useAppSelector(selectState);
    const current = useAppSelector(selectCurrent);
    const tasks: Record<string, TaskType> = useAppSelector(selectTasks);
    const dispatch = useAppDispatch();

    useEffect(() => {
        ...
        // request new tasks if we either have not tasks yet or options changed and we
        // are not currently loading them
        if (StateTypes.IDLE === state) {
            // lookup tasks
            dispatch(requestTasksAsync({uri: "http://localhost:8080/api", pageOptions: pageOptions, filterOptions: filterOptions}));
        }
        ...

    }, [dispatch, state, tasks, pageOptions, filterOptions])

上面的代码有效。我能够根据 link 关系查找 URI 并获取正确的 URI 以检索任务资源公开的数据并将这些信息存储到 Redux 存储中。然而,这一切都感觉非常笨拙,因为我必须 return 来自动作创建的两个响应对象,因为我既不允许从非功能组件内部发出调度,也不允许改变动作本身的状态。

理想情况下,我喜欢从异步 thunk 中以某种方式分派操作,并有一个回调,一旦数据在商店中可用,就会通知我,但我想这是不可能的。由于我对 React/Redux 还很陌生,我想知道是否有更好的方法可以根据某些依赖项触发操作,并且在没有这些依赖项的情况下加载这些依赖项?我知道虽然我可以简单地将操作分成单独的操作,然后在各自的组件内进行调用,但感觉它会 a) 将切片负责的一些状态管理逻辑拖到各自的组件中b) 复制一些代码。

您完全可以从 asyncThunk 内部调度。

export const requestTasksAsync = createAsyncThunk<[APITasksResponse, APILinkResponse | undefined], { uri: string; pageOptions?: PageOptions; filterOptions?: FilterOptions; }, { state: ApiState }>(
    'api/tasks',
    async ({ uri, pageOptions, filterOptions }, { getState, dispatch }) => {
        const state: ApiState = getState();
        const TASK_REL = 'http://acme.com/rel/tasks'; // RFC 8288 compliant link relation extension according to section 2.1.2

        dispatch(whatever())

        return await lookup<APITasksResponse>(state, uri, TASK_REL, pageOptions, filterOptions);
    }
);

此外,您不需要在通用和有效负载生成器函数中重复 arg 类型。

在你的情况下,要么使用泛型(然后也将 ApiState 类型向上移动到泛型中),要么跳过顶部的泛型定义并内联输入所有内容。