I saw two different ways to handle async operation in redux-toolkit. I want to know is there any difference and what are the difference, and which is the best way?
Option 1:
user-actions.js
export const getUsers = () => {
return async (dispatch) => {
try {
dispatch(userActions.setLoading(true));
const data = await getDealershipUsers();
dispatch(userActions.setUsers({ data }));
dispatch(userActions.setLoading(false));
} catch (err) {}
};
};
user-slice.js
setUsers(state, action) {
const { users } = action.payload.data;
state.users = users;
}
Option 2 - with createAsyncThunk:
user-slice.js
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await axios.get(url)
return response.data
})
// ...and then in extraReducers...
extraReducers(builder) {
builder
.addCase(fetchUsers.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded'
state.users = state.users.concat(action.payload)
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
Related
I have an array of incomes that I display in a table. There's a "+" button to add a new income. I want to increment database and redux store with a dispatch.
Increment database works fine, but I have to reload page to see new data.
Why my dispatch does not update the state.data of incomes ?
// AddNewData.tsx
[...]
const onSubmit: SubmitHandler<FieldValues> = async (data) => {
if (user) {
const formData = {
userId: user._id,
label: data.label,
amount: data.amountInput,
};
dispatch(addUserIncome(formData));
}
};
[...]
//incomes_slice.ts
import { IDataState, IEntry } from "../../helpers/Interfaces";
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { deleteData, getData, updateData, addData } from "../../APIs/UserData";
import { Notifier } from "../../helpers/Notifier";
const initialState: IDataState = {
data: [],
loading: false,
error: null,
};
export const getUserIncomes = createAsyncThunk("incomes/getUserIncomes", (userId: string) => {
return getData("incomes", userId);
});
export const addUserIncome = createAsyncThunk("incomes/addUserIncome", async (data: { userId: string; label: string; amount: number }) => {
const response = await addData("incomes", data).then((res) => {
const newEntry: IEntry = {
_id: res.data.income._id,
user: data.userId,
label: data.label,
amount: data.amount,
};
return newEntry;
});
return response;
});
export const updateUserIncome = createAsyncThunk("incomes/updateUserIncome", async (data: { id: string; data: IEntry }) => {
await updateData("incomes", data.id, data.data);
return data;
});
export const deleteUserIncome = createAsyncThunk("incomes/deleteUserIncome", async (incomeId: string) => {
await deleteData("incomes", incomeId);
return incomeId;
});
const incomesSlice = createSlice({
name: "incomes",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getUserIncomes.pending, (state) => {
state.loading = true;
});
builder.addCase(getUserIncomes.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
state.error = null;
});
builder.addCase(getUserIncomes.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
action.error.message && Notifier(action.error.message, "error");
});
builder.addCase(addUserIncome.fulfilled, (state, action) => {
state.data.push(action.payload);
});
builder.addCase(updateUserIncome.fulfilled, (state, action) => {
state.data = state.data.map((income) => {
if (income._id === action.payload.id) {
return action.payload.data;
}
return income;
});
});
builder.addCase(deleteUserIncome.fulfilled, (state, action) => {
state.data = state.data.filter((income) => income._id !== action.payload);
});
},
});
export const incomesReducer = incomesSlice.reducer;
PS: addData function is just an axios.post that works fine.
Inside addUserIncome you are mixing styles of awaiting a promise.
Instead of this:
const response = await addData("incomes", data).then((res) => {
const newEntry: IEntry = {
_id: res.data.income._id,
user: data.userId,
label: data.label,
amount: data.amount,
};
return newEntry;
});
Either use this:
try {
const response = await addData("incomes", data);
// do stuff
} catch (e) {
console.log(e);
}
Or do this:
addData("incomes", data).then((res) => {
// do stuff
})
I can't leave a comment (just don't have enough credit yet).
You said you checked your store? Are you sure that the data is changed in the store?
(Because this is async of course, and you're not managing loading and pending state of your call, probably your UI is rendered before the data is updated.)
The issue I have is handling the addCase for each of the async calls. For every async call I have, do I need to keep adding the pending and fulfilled cases to the builder under extraReducers or is there another way?
Code for context:
export const counterSlice = createSlice({
...
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state, action) => {
state.status = 'loading'
console.log(action);
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle'
state.value += action.payload
console.log(action);
})
.addCase(getWorkouts.pending, (state, action) => {
state.status = 'loading'
console.log(action);
})
.addCase(getWorkouts.fulfilled, (state, action) => {
state.status = 'idle'
state.data = action.payload
console.log(action);
})
// ... and so on?
},
})
where incrementAsync and getWorkouts are async thunks:
export const incrementAsync = createAsyncThunk(
FETCH_COUNT,
async (amount: number) => {
const response = await fetchCount(amount)
// The value we return becomes the `fulfilled` action payload
return response.data
}
)
export const getWorkouts = createAsyncThunk(
FETCH_WORKOUTS,
async (payload: { userId: string, date: Date }) => {
const response = await getWorkoutsAsync(payload.userId, payload.date);
// The value we return becomes the `fulfilled` action payload
return response;
}
)
Tryed to render component using useEffect with "product" dependency but it goes cyclic dependency.
Tryed to use prev state but it doesn't help.
React don't gives any error, but useEffect send request every second.
Component:
export const ProductPage = (props: any) => {
const { product, isLoading, error } = useTypedSelector(state => state.Product)
const dispatch = useTypedDispatch()
const { id } = useParams()
const prevProd = usePrevious(product)
useEffect(() => {
if (prevProd !== product){dispatch(fetchProduct(Number(id)))}
}, [product])
return (
<div>
<div>{product.name}</div>
<div>{product.id}</div>
<div>{product.price}</div>
</div>
)
}
Async Thunk:
export const fetchProduct = createAsyncThunk(
'product/fetch',
async (id: number, thunkApi) => {
try {
const response = await productService.fetch(id)
return response.data
} catch (error: any) {
return thunkApi.rejectWithValue(error.message)
}
}
)
Slice:
export const ProductSlice = createSlice({
name: 'product',
initialState,
reducers: {},
extraReducers: {
[fetchProduct.fulfilled.type]: (state, action: PayloadAction<IProductData>) => {
state.error = ''
state.isLoading = false
state.product = action.payload
},
[fetchProduct.pending.type]: (state) => {
state.isLoading = true
},
[fetchProduct.rejected.type]: (state, action: PayloadAction<string>) => {
state.isLoading = false
state.error = action.payload
},
}
})
Explain please, why this problem occured and how resolve this?
I'm using the rudux toolkit in react. After rendering the first page, useEffect is executed and the farmloadPost action is executed.
By the way, armloadPost.pending is executed three times. How can I make it run only once?
i tried disabled stricmode, but it same
this is my code
useEffect( () => {
dispatch(farmloadPost());
}, [dispatch]);
export const farmloadPost = createAsyncThunk(
"farm/farmloadPost",
async (data, { rejectWithValue }) => {
try {
const response = await axios.get(api);
return response.data;
} catch (error: any) {
console.log("error:",error);
return rejectWithValue(error.response.data);
}
}
);
const postSlice = createSlice({
name: "post",
initialState,
reducers: {},
extraReducers: (builder) =>
builder
// loadPost
.addCase(farmloadPost.pending, (state) => {
state.farmLoading = true;
state.farmDone = false;
state.farmError = null;
})
.addCase(farmloadPost.fulfilled, (state, action) => {
// console.log("action.payload:", action.payload);
state.farmLoading = false;
state.farmDone = true;
state.farm = action.payload;
})
.addCase(farmloadPost.rejected, (state, action) => {
state.farmLoading = false;
// state.farmDone = action.error.message;
})
.addDefaultCase((state) => state),
});
I use react redux redux-toolkit
When switching to CardDetails, everything works and I get data in the state of only one element, but when I go from this page to any other page that has useEffect and in which I use the same state, it does not work and I have to reload the page manually. For what reason can this happen?
Here is the page code for one element
const CardDetails = () => {
const {id} = useParams()
const dispatch = useDispatch()
const location = useLocation()
const {posts, loading} = useSelector((state) => ({...state.posts}))
useEffect(() => {
dispatch(getPost(id))
}, []);
console.log(posts)
return (
<div>
<img style={{maxWidth: "150px"}} src={posts.img} alt="image"/>
</div>
);
};
Here is the page I go to from CardDetails
const Main = () => {
const dispatch = useDispatch()
const {posts, loading} = useSelector((state) => ({...state.posts}))
const user = JSON.parse(localStorage.getItem('profile'))
useEffect(() => {
dispatch(getPosts())
}, []);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [creator, setCreator] = useState("");
const [category, setCategory] = useState("");
const [img, setImg] = useState("");
const [tags, setTags] = useState(["tag1", "tag2"]);
const onSavePostClicked = () =>{
const newPost = {title, description, category, img}
dispatch(createPost(newPost))
setTitle('')
setDescription('')
setCreator('')
setCategory('')
setImg('')
}
return (
And here is the postsSlice
import {createAsyncThunk, createSlice} from "#reduxjs/toolkit";
import * as api from "../../../api/index";
import {fetchPost, fetchPostsCategory} from "../../../api/index";
export const getPosts = createAsyncThunk(
'posts/getPosts',
async (thunkAPI)=> {
const response = await api.fetchPosts()
return response.data.data
})
export const getPost = createAsyncThunk(
'posts/getPost',
async (id,thunkAPI)=> {
const response = await api.fetchPost(id)
return response.data.data
})
export const createPost = createAsyncThunk(
'posts/createPosts',
async (newPost,thunkAPI)=> {
const response = await api.createPost(newPost)
return response.data.data
})
export const deletePost = createAsyncThunk(
'posts/deletePost',
async (post,thunkAPI)=> {
const {_id} = post
const response = await api.deletePost(_id)
return response.data
})
export const updatePost = createAsyncThunk(
'posts/updatePost',
async (post,thunkAPI)=> {
const {_id} = post
const postEdited = {...post, published: true}
const response = await api.updatePost(_id, postEdited)
return response.data
})
export const getCategory = createAsyncThunk(
'posts/getCategory',
async (category,thunkAPI)=> {
const response = await api.fetchPostsCategory(category)
return response.data.data
})
const initialState = {
posts: [],
loading: false,
error:null
}
export const postsSlice = createSlice({
name: "posts",
initialState,
reducers:{
postAdded: {
reducer(state, action) {
state.posts.push(action.payload)
},
prepare(title, description, creator, category, img) {
return {
payload: {
title,
description,
creator,
category,
img
}
}
}
},
},
extraReducers: {
[createPost.pending]: (state, action) => {
state.loading = true;
},
[createPost.fulfilled]: (state, action) => {
state.loading = false;
state.posts.push(action.payload);
},
[createPost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getPosts.pending]: (state) => {
state.loading = true
},
[getPosts.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPosts.rejected]: (state) => {
state.loading = false
},
[deletePost.fulfilled]: (state, action)=>{
if (!action.payload?._id) {
console.log('Delete could not complete')
console.log(action.payload)
return;
}
const { _id } = action.payload;
state.posts = state.posts.filter(post => post._id !== _id);
},
[updatePost.pending]: (state, action) => {
state.loading = true;
},
[updatePost.fulfilled]: (state, action) => {
const { _id } = action.payload;
state.loading = false;
const posts = state.posts.filter(post => post._id !== _id);
state.posts = [...posts, action.payload];
},
[updatePost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getCategory.pending]: (state) => {
state.loading = true
},
[getCategory.fulfilled]: (state, action) => {
state.loading = false
console.log()
state.posts = action.payload
},
[getCategory.rejected]: (state) => {
state.loading = false
},
[getPost.pending]: (state) => {
state.loading = true
},
[getPost.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPost.rejected]: (state) => {
state.loading = false
},
},
})
export const {postAdded} = postsSlice.actions
export default postsSlice.reducer
When you are using useEffect with an empty array as deps [] it will only be trigger when you mount your component (and cleaned when you unmount the component)