From a Redux tutorial I've been going through they allow you to add a place multiple times. I changed the reducer to reject duplicates. My question is, (see code), do I have to return the state if no updates are made or is there some other way of indicating no state is changed?
function placeReducer(state = initialState, action) {
switch (action.type) {
case ADD_PLACE:
const existing = state.places.find((item) => item.value == action.payload);
if (existing) {
return {...state};
}
return {
...state,
places: state.places.concat({
key: Math.random(),
value: action.payload
})
};
default:
return state;
}
}
Just return the state, no need to create a new copy.
Related
I am implementing a shop cart using react-redux.
I got two reducers,
1.To fetch cart data from DB
2. To Carry out various cart operations.
My doubt is after achieving data from DB through the first reducer, how will I access that data through the 2nd reducer in order to carry out different cart operations ?
Reducer 1 - Fetch Data from DB
const initialState={
loading:false,
items:[],
error:false
}
const CartFetch=(state=initialState,action)=>{
switch(action.type){
case FETCHDATA : return {
...state,loading:true ,error:false
};
case FETCHSUCCESS: return {
...state,loading:false,
items:[...action.payload]
};
case FETCHERROR : return {
...state,loading:false,error:true
};
default: return state;
}
}
Fetch Actions
const fetch=()=>{
return {
type:FETCHDATA
}
}
const success=(user)=>{
return {
type:FETCHSUCCESS,
payload:user
}
}
const error=()=>{
return {
type:FETCHERROR
}
}
const fetchCartData=()=>{
const {id}=getCurrentUser();
return (dispatch)=>{
dispatch(fetch());
axios.get(`${api.userOperations}/cart/${id}`,{
headers:{'Authorization': getJwt()}
}).then(({data})=>{
dispatch(success(data));
}).catch(()=>{
dispatch(error())
})
}
}
Reducer 2 - Cart Operations
const CartHandle=(state= ..?.. ,action)=>{
switch(action.type){
case ADD_TO_CART :
return {
......
};
case INCREMENT_CART : return {
....
};
case DECREMENT_CART: return {
......
};
case REMOVE_FROM_CART : return {
.....
};
default: return state;
}
}
}
Here in Reducer 2 how will I access the pass the data which I fetched in Reducer 1 ? Or easy there any better way of implementing what I m trying to ?
Combine Reducers
const allReducer=combineReducers({
Cart:CartFetch,
CartOperations: CartHandle
});
Store
const countStore=createStore(allReducer,applyMiddleware(thunk));
<Provide store={store}>
...App.js...
</Provider>
Issue
It seems you don't quite fully understand what a reducer represents. Each reducer represents a specific "chunk" or slice of state. No two reducers function/operate on the same slice of state. In other words, two separate reducers equals two separate slices of state.
Solution
Since a reducer represents a specific slice of state it needs to handle all the actions that are associated with that slice. You just need to merge your second reducer into the first on so it fully manages the cart state.
const initialState = {
loading: false,
items: [],
error: false
};
const cartReducer = (state = initialState, action) => {
switch (action.type) {
case FETCHDATA:
return {
...state,
loading: true,
error: false
};
case FETCHSUCCESS:
return {
...state,
loading: false,
items: [...action.payload]
};
case FETCHERROR:
return {
...state,
loading: false,
error: true
};
case ADD_TO_CART:
return {
// ......
};
case INCREMENT_CART:
return {
// ....
};
case DECREMENT_CART:
return {
// ......
};
case REMOVE_FROM_CART:
return {
// .....
};
default:
return state;
}
};
Create your root reducer, each combined reducer represents a slice of state.
const allReducer = combineReducers({
// ... other state slice reducers
cart: cartReducer,
// ... other state slice reducers
});
I've noticed that in many useReducer examples, the spread operator is used in the reducer like this:
const reducer = (state, action) => {
switch (action.type) {
case 'increment1':
return { ...state, count1: state.count1 + 1 };
case 'decrement1':
return { ...state, count1: state.count1 - 1 };
case 'increment2':
return { ...state, count2: state.count2 + 1 };
case 'decrement2':
return { ...state, count2: state.count2 - 1 };
default:
throw new Error('Unexpected action');
}
};
However in many of my practices, I removed ...state and had no issues at all. I understand that ...state is used to preserve the state of the remaining states, but would a reducer preserve those states already so the ...state is not needed?
Can someone give me some examples where ...state is a must and causes issues when removed with useReducer hook? Thanks in advance!
No, a reducer function alone would not preserve existing state, you should always be in the habit shallow copy existing state. It will help you avoid a class of state update bugs.
A single example I can think of when spreading the existing state may not be necessary is in the case where it isn't an object.
Ex: a "count" state
const reducer = (state = 0, action) => {
// logic to increment/decrement/reset state
};
Ex: a single "status" state
const reducer = (state = "LOADING", action) => {
// logic to update status state
};
Spreading the existing state is a must for any state object with multiple properties since a new state object is returned each time, in order to preserve all the existing state properties that are not being updated.
Edit 1
Can you give an example when NO shallow copying causing state update bugs?
const initialState = {
data: [],
loading: false,
};
const reducer = (state, action) => {
switch(action.type) {
case LOAD_DATA:
return {
...state,
loading: true,
};
case LOAD_DATA_SUCCESS:
return {
...state,
data: action.data,
loading: false
};
case LOAD_DATA_FAILURE:
return {
loading: false,
error: action.error,
};
default:
return state;
}
};
As can been seen in this example, upon a data load failure the reducer neglects to copy the existing state into the new state object.
const [state, dispatch] = useReducer(reducer, initialState);
...
useEffect(() => {
dispatch({ type: LOAD_DATA });
// logic to fetch and have loading failure
}, []);
return (
<>
...
{state.data.map(...) // <-- throws error state.data undefined after failure
...
</>
);
Any selector or UI logic that assumes state.data always exists or is always an array will fail with error. The initial render will work since state.data is an empty array and can be mapped, but upon a loading error state.data is removed from state.
Link to the repo: https://github.com/charles7771/ugly-code
At the 'Options' component, I am not writing anything dynamically for a form I'm creating, I am giving each one of the reducer cases a different name. That is not scalable at all and I have no idea how to go about fixing it. Any thoughts?
It is something like this:
case 'SET_SMALLPRICE0': //goes from 0 to over 20
return {
...state,
smallPrice0: action.payload,
}
case 'SET_MEDIUMPRICE0':
return {
...state,
mediumPrice0: action.payload,
}
You could name the action type the exact name of the property to update, or perhaps with a "set_" in front. That would allow you to automatically set that property.
const reducerPriceCarSizeEachService = (state, action) => {
return { ...state, [action.type]: action.payload }
}
or, with a "set_" in front (this could allow you to use different prefixes, such as "push_", "delete_" depending on what you actually want to do)
const reducerPriceCarSizeEachService = (state, action) => {
if (action.type.startsWith('set_')) {
let prop = action.substr(4)
return { ...state, [props]: action.payload }
}
return state
}
I'm combining the slice of state below (filterText), it's use to filter out results so it only needs to hold a string. Is it possible to just have the initial state be an empty string? Or does it have to be an object even though it's just a slice of the overall larger store object? If I can have it as a string, how do I make a new copy of the state each for each dispatch? The current return {...state, ...action.data} looks weird.
const initialState = ''
const filterText = (state = initialState, action) => {
switch (action.type) {
case constants.FILTER_CONTACTS:
return {
...state,
...action.data
}
default:
return state
}
}
export default filterText
The initial state can be a string, but then in every switch case it should also return a string.
When updating the state, you would not need to make a copy since your entire state is a string you would just return the new string. If there are no changes, you would just return the old state.
const initialState = ''
const filterText = (state = initialState, action) => {
switch (action.type) {
case constants.FILTER_CONTACTS:
// return a string here, I'm assuming action.data is a string
return action.data;
default:
return state
}
}
export default filterText
Hope this helps.
I seem to have hit a snag when updating state using redux and react-redux. When I update an individual slice of state, all of the others get removed. I know the answer to this will be simple but I can't figure it out and haven't found anything else online.
So to clarify, here's my reducer:
const initialState = {
selectedLevel: null,
selectedVenue: null,
selectedUnitNumber: null,
selectedUnitName: null,
selectedYear: null
}
export default (state = initialState, action) => {
console.log('reducer: ', action);
switch (action.type){
case 'CHOOSE_LEVEL':
return action.payload;
case 'CHOOSE_VENUE':
return action.payload;
case 'CHOOSE_UNIT':
return action.payload;
case 'SHOW_COURSES':
return action.payload;
}
return state;
}
And my combine reducer:
export default combineReducers({
workshopSelection: WorkshopSelectReducer
});
So my initial state looks like this:
workshopSelection: {
selectedLevel: null,
selectedVenue: null,
selectedUnitNumber: null,
selectedUnitName: null,
selectedYear: null
}
But when I use one of my action creators, for example:
export function chooseVenue(venue){
return {
type: 'CHOOSE_VENUE',
payload: {
selectedVenue: venue
}
}
}
I end up with state looking like this:
workshopSelection: {
selectedVenue: 'London',
}
All of the rest of the state within this object that wasn't affected by this action creator has been completely wiped out. Instead, I just want all other entries to stay as they are with their original values - null in this example, or whatever other value has been assigned to them.
Hope that all makes sense.
Cheers!
You are basically replacing one object (previous state) with another one (your payload, which is also an object).
In terms of standard JS, this would be the equlivalent of what your reducer does:
var action = {
type: 'CHOOSE_VENUE',
payload: {
selectedVenue: venue
}
};
var state = action.payload;
The simplest way to fix this would be using Object spread properties:
export default (state = initialState, action) => {
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
// Watch out, fall-through used here
return {
...state,
...action.payload
};
}
return state;
}
... but since this is still in experimental phase, you have to use some other way to clone previous properties and then override the new ones. A double for ... in loop could be a simple one:
export default (state = initialState, action) => {
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
// Watch out, fall-through used here
const newState = {};
// Note: No key-checks in this example
for (let key in state) {
newState[key] = state[key];
}
for (let key in action.payload) {
newState[key] = action.payload[key];
}
return newState;
}
return state;
}
Keep your payload object as flat on actions creators as shown below...
export function chooseVenue(venue){
return {
type: 'CHOOSE_VENUE',
selectedVenue: venue
}
}
and modify your reducer as below (given example is for updating the venue, do the same for other cases too...)
export default (state = initialState, action) => {
let newState = Object.assign({}, state); // Take copy of the old state
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
newState.selectedVenue = action.selectedVenue; // mutate the newState with payload
break;
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
default :
return newState;
}
return newState; // Returns the newState;
}