通过 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 指定的关系(即 up
、next
, 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
类型向上移动到泛型中),要么跳过顶部的泛型定义并内联输入所有内容。
在 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 指定的关系(即 up
、next
, 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
类型向上移动到泛型中),要么跳过顶部的泛型定义并内联输入所有内容。