I saw this https://redux-toolkit.js.org/api/createAsyncThunk#canceling-while-running
but I have two dispatch functions inside an onClick handler.
How can I put them into useEffect()?
I want to achieve when user quickly clicks on another item, the previous fetchFileContentsHandler will be cancelled, and always return the result from the latest invoke.
I tried to wrap fetchFileContentsHandler in useCallback(),
and put fetchFileContentsHandler inside useEffect() with clean up return.
but it resulted in strange behavior.
const fetchFileContentsHandler = () => {
dispatch(fetchFileContents(props.params));
dispatch(
changeSelectedFile({ selectedFile: props.params, selected: true })
);
};
return (
<NavLink to="#" key={fileName}>
<ListItemButton
selected={props.selected}
onClick={fetchFileContentsHandler}
>
<ListItemText
primary={fileName}
/>
</ListItemButton>
</NavLink>
);
};
export default Items;
[update]
I tried to change fetchFileContents to createAsyncThunk and add the suggested useRef() and .abort(), but it didn't work.
If the first fetchFileContents takes longer than the second fetchFileContents, it still updates the htmlReport or textReport value once it's fulfilled.
Then once htmlReport or textReport value changes, I display its contents in another component. So I can still see the first fetched file contents display after the second fetched file contents.
slice.js
export const fetchFileContents = createAsyncThunk(
"ui/getFileContents",
async (fileName) => {
const fileContentsResponse = await fetch(...);
const response = await fileContentsResponse.json();
return {
fileType: fileName.includes("html") ? "text" : "html",
contents: data,
};
}
);
const Slice = createSlice({
name: "somename",
initialState: {
htmlReport: null,
textReport: null,
},
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(fetchFileContents.fulfilled, (state, action) => {
if (action.payload.fileType === "text") {
state.textReport = action.payload.contents;
state.htmlReport = null;
} else if (action.payload.fileType === "html") {
state.textReport = null;
state.htmlReport = action.payload.contents;
}
})
.addCase(fetchFileContents.rejected, (state, action) => {
console.log("[REJECTED]: ");
const { fileName } = action.meta.arg;
const { message } = action.error;
console.log(fileName, message);
});
},
});
Component.js
const fetchFileContentsPromise = useRef();
const fetchFileContentsHandler = () => {
fetchFileContentsPromise.current?.abort();
fetchFileContentsPromise.current = dispatch(
fetchFileContents({ fileName: props.params })
);
dispatch(
changeSelectedFile({ selectedFile: props.params, selected: true })
);
};
[update] I can't find any major differences between your codes and mine. Your log was correctly recorded:
undefined
Fetching data with A...
Promise .....{fileName: 'A'.....
Fetching data with C...
[REJECTED]: A, I canceled it.
Promise resolved: CCC
[FULFILLED]: CCC
Promise resolved: AAA
But my log looks like this:
undefined
Fetching data with A...
undefined
Fetching data with C...
[FULFILLED]: CCC
[Violation] 'setTimeout' handler took 55ms
[FULFILLED]: AAA
or when I repeat:
Promise .....{fileName: 'A'.....
Fetching data with A...
Promise .....{fileName: 'C'.....
Fetching data with C...
[FULFILLED]: CCC
[Violation] 'setTimeout' handler took 55ms
[FULFILLED]: AAA
[update -- Solved]
I figured out that I had to declare const fetchFileContentsPromise = useRef(); in the parent component and use props to send it to the child then send it to child's child again.
I use a nested object and pick the necessary values to parent -> child(1) -> child(2).
This fetchFileContents is in child(2).
But I don't understand why.
I would be really appreciated it if anyone could explain.