【问题标题】:Processing action-creatos with dependencies in React via redux-toolkit通过 redux-toolkit 在 React 中处理具有依赖关系的 action-creatos
【发布时间】:2021-10-17 22:57:55
【问题描述】:

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

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

我目前使用 ReduxJS 工具包和切片结合异步 thunk 来管理和访问 Redux 存储。存储本身有一个专门用于后端 API 返回的内容的部分,即全局链接部分、页面信息等,并将存储每次调用的结果并稍作重新映射。

我在这里面临的一个挑战是,在最初请求负责呈现所有任务的组件时,该组件首先需要在后端 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 标识的资源返回的链接,以防它们不存在于 Redux 存储或状态中或已过时。如果我们在跳过此步骤之前收集了链接,而是重用了 state/store 中可用的信息(所有这些都发生在 requestLinks(...) 函数中)。一旦链接可用,我们最终可以请求为我们想要获取的任务信息提供服务的资源。

上面的 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,因此链接响应是未定义的,我们只需使用 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;
    }
}

根据是使用自定义链接关系还是由 IANA 指定的链接关系(即upnextprev、...),将链接分为全局 URI 和组件 URI。

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])

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

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

【问题讨论】:

    标签: reactjs typescript redux redux-toolkit


    【解决方案1】:

    您可以完全从 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 类型向上移动到泛型中),或者跳过顶部的泛型定义并内联键入所有内容。

    【讨论】:

    • 我错过了您可以为createAsyncThunk 函数提供ThunkApiConfig。我不得不将这里的状态定义更改为{ state: RootState },并在正文中更改为getState().api,以便调度真正起作用。我之前在尝试从动作创建者分派时导入了useAppDispatcher,这导致了一个错误,因为这仅允许在功能组件中进行,因为我不知道ThunkApiConfig 提供了回调。但是,我如何在 thunk 中等待正在写入的状态?我需要对州进行民意调查吗?
    • 你在那里等待什么“书面状态”?作为派遣的结果?如果是正常的 reducer 动作,那将发生在下一行
    猜你喜欢
    • 2017-01-15
    • 2020-07-18
    • 2022-01-26
    • 2015-05-11
    • 1970-01-01
    • 2018-07-23
    • 2018-11-28
    • 2020-05-05
    • 1970-01-01
    相关资源
    最近更新 更多