I've been working on an issue that involves making a call in uploading a file via an asyncThunk call. Once that call is complete, it returns a payload of the filePath.
From there I am attempting to update the state of an object called "transaction" within the "addCase" code block for when the uploadFileAsyncThunk.fulfilled so that it can be saved in the database. Everything functions as it should, except the order in which the code executes is not how I would expect it to be. It appears that the state does not update at the time the transaction object is sent to the second asyncThunk (postTransactionThunk).
Debugging through the code, I can see that the uploadResultAction is populated with the return data I expect. and the uploadFileThunk.fulfilled.match(uploadResultAction)
is valid to proceed through the rest of the code. From there, getting the filePathParam does contain the data I want and dispatching the data does work. I can console log at the top of the component that my data has been updated. However, when I see the state through Redux DevTools. My state doesn't update until dispatchPostTransactionThunk
is executing, which at that point is too late because the transaction data being referenced for POST is outdated.
How do I update my transaction
state so that I am able to execute postTransactionThunk with the most updated data?
const MyComponent = () => {
const transaction = useAppSelector(
(state) => state.transaction.transaction
);
const handleClick = async (e) => {
e.preventDefault();
// returned value is accurate
const uploadResultAction = await dispatch(uploadFileThunk(file));
// condition passes and enters code block
if (uploadFileThunk.fulfilled.match(uploadResultAction)) {
// filePathParam has the payload I want.
const filePathParam =
{
filePath: uploadResultAction.payload.response,
};
// dispatch executes updating transaction.filePath.
dispatch(updateTransaction(filePathParam));
// ISSUE: transaction does not have the transaction.filePath updated; filePath is still an empty string.
const postTransactionResultAction = await dispatch(postTransactionThunk(transaction));
if (postTransactionThunk.fulfilled.match(
postTransactionResultAction)) {
// ...
}
}
}
return (
<div>
<Button onClick={handleClick}>Click</Button>
</div>
);
}
export default MyComponent;
Here is supplemental logic for updating the transaction upon fulfilled:
const initialTransactionState = {
filePath: '',
};
const initialState = {
transaction: initialTransactionState,
fileUploadResponse: null,
loading: false,
error: null,
};
export const postTransactionThunk = createAsyncThunk(
'postTransactionThunk',
async (transaction, { dispatch }) => {
const response = await dispatch(
transactionApiSlice.endpoints.postTransaction.initiate(transaction)
);
return response.data;
}
);
export const uploadFileThunk = createAsyncThunk(
'uploadFileThunk',
async (file, { dispatch }) => {
const response = await dispatch(
fileApiSlice.endpoints.postFile.initiate(file)
);
return response.data;
}
);
export const transactionSlice = createSlice({
name: 'transaction',
initialState,
reducers: {
updateTransaction: (state, action) => {
return {
...state,
transaction: { ...state.transaction, ...action.payload },
};
},
},
extraReducers: (builder) => {
builder
.addCase(uploadFileThunk.fulfilled, (state, action) => {
return {
...state,
loading: false,
error: null,
fileUploadResponse: action.payload,
transaction: {
...state.transaction,
filePath: action.payload.response,
},
};
})
},
});
export const {
updateTransaction,
} = transactionSlice.actions;
export default transactionSlice.reducer;
I've tried debugging through the code. I've also tried placing cascading .then(...) blocks between each dispatch call which didn't make a difference either.
The issue appears to be a stale closure over the transaction
value that is updated by the updateTransaction
action mid-stream and passed to postTransactionThunk
.
I suggest updating postTransactionThunk
to access the current state value directly instead of depending on the passed value which is possibly stale. In the thunk you can use thunkApi.getState
to access the current state value
export const postTransactionThunk = createAsyncThunk(
'postTransactionThunk',
async (_, { dispatch, getState }) => {
const state = getState();
const transaction = state.transaction.transaction;
const { data } = await dispatch(
transactionApiSlice
.endpoints
.postTransaction
.initiate(transaction)
).unwrap();
return data;
}
);
The UI code then doesn't need to have the current transaction
state, use dispatch(postTransactionThunk())
.
const handleClick = async (e) => {
e.preventDefault();
const uploadResultAction = await dispatch(uploadFileThunk(file));
if (uploadFileThunk.fulfilled.match(uploadResultAction)) {
const filePathParam = {
filePath: uploadResultAction.payload.response,
};
dispatch(updateTransaction(filePathParam));
const postTransactionResultAction = await dispatch(postTransactionThunk());
if (
postTransactionThunk.fulfilled.match(postTransactionResultAction)
) {
// ...
}
}
};
I suggest also using the Thunks to their full potential. Instead of using matchers locally you can await and unwrap the thunk results. See Handling Thunk Results for details.
Something like the following:
const handleClick = async (e) => {
e.preventDefault();
try {
const {
response: filePath
} = await dispatch(uploadFileThunk(file)).unwrap();
const filePathParam = { filePath };
dispatch(updateTransaction(filePathParam));
await dispatch(postTransactionThunk()).unwrap();
// ... postTransactionThunk success, keep going
} catch(error) {
// rejected Promise, thrown error, postTransactionThunk failure
}
};