React redux counter example from a array of objects - javascript

I am trying to create an example of a counter with React and Redux, but I can not update the status of the current item in which it was clicked.
The click event, i pass the id in the payload of the current item clicked.
return this.props.peliculas.map(movie => {
return <li onClick={() => this.handleClicks(movie.id)} key=
{movie.id}>{movie.title}</li>
});
I have function in the class to handle the event:
handleClicks(peli){
this.props.onLiClick(peli);
}
The dispatch part:
const mapStateToProps = state => {
return {
peliculas: state.movies.peliculas
}
};
const mapDispatchToProps = dispatch => {
return {
onLiClick: (id) => dispatch({ type: 'ADD_CLICK', payload: {id} })
}
};
The reducer
const laspelis = {
peliculas: [{title: 'T1', id: 1, clicks: 0}, {title: 'T2', id: 2, clicks: 0}],
isActive: false
};
export const movies = (state= laspelis, action) => {
switch (action.type) {
case 'ADD_CLICK':
//How to update the current item inside of the reducer?
// After click the current item add 1 to the clicks property
// If the item has id: 2 => {title: 'T2', id: 2, clicks: 1}
return {
...state,
peliculas: [{title: 'Otro 1', id:1},{title: 'Otro 2', id:2}]
}
default:
break;
}
return state;
};
I have the click event linked correctly and the action is sent to the reducer,(I'm just going to show the code partially)
Thanks.

You need to find an item to update by id, replace it with a new one, and not to forget to change entire array
return {
...state,
peliculas: state.peliculas.map(item => {
if(item.id === payload.id) {
return { ...item, clicks: item.clicks + 1}
}
return item;
})
}

Related

How to update state with usestate in an array of objects?

I'm having some trouble with the React useState hook. I have a todolist with a checkbox button and I want to update the 'done' property to 'true' that has the same id as the id of the 'clicked' checkbox button. If I console.log my 'toggleDone' function it returns the right id. But I have no idea how I can update the right property.
The current state:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: false
},
]
})
const toggleDone = (id) => {
console.log(id);
}
return (
<div className="App">
<Todos todos={state.todos} toggleDone={toggleDone}/>
</div>
);
}
The updated state I want:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: true // if I checked this checkbox.
},
]
})
You can safely use javascript's array map functionality since that will not modify existing state, which react does not like, and it returns a new array. The process is to loop over the state's array and find the correct id. Update the done boolean. Then set state with the updated list.
const toggleDone = (id) => {
console.log(id);
// loop over the todos list and find the provided id.
let updatedList = state.todos.map(item =>
{
if (item.id == id){
return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
}
return item; // else return unmodified item
});
setState({todos: updatedList}); // set state to new object with updated list
}
Edit: updated the code to toggle item.done instead of setting it to true.
You need to use the spread operator like so:
const toggleDone = (id) => {
let newState = [...state];
newState[index].done = true;
setState(newState])
}
D. Smith's answer is great, but could be refactored to be made more declarative like so..
const toggleDone = (id) => {
console.log(id);
setState(state => {
// loop over the todos list and find the provided id.
return state.todos.map(item => {
//gets everything that was already in item, and updates "done"
//else returns unmodified item
return item.id === id ? {...item, done: !item.done} : item
})
}); // set state to new object with updated list
}
const toggleDone = (id) => {
console.log(id);
// copy old state
const newState = {...state, todos: [...state.todos]};
// change value
const matchingIndex = newState.todos.findIndex((item) => item.id == id);
if (matchingIndex !== -1) {
newState.todos[matchingIndex] = {
...newState.todos[matchingIndex],
done: !newState.todos[matchingIndex].done
}
}
// set new state
setState(newState);
}
Something similar to D. Smith's answer but a little more concise:
const toggleDone = (id) => {
setState(prevState => {
// Loop over your list
return prevState.map((item) => {
// Check for the item with the specified id and update it
return item.id === id ? {...item, done: !item.done} : item
})
})
}
All the great answers but I would do it like this
setState(prevState => {
...prevState,
todos: [...prevState.todos, newObj]
})
This will safely update the state safely. Also the data integrity will be kept. This will also solve the data consistency at the time of update.
if you want to do any condition do like this
setState(prevState => {
if(condition){
return {
...prevState,
todos: [...prevState.todos, newObj]
}
}else{
return prevState
}
})
I would create just the todos array using useState instead of another state, the key is creating a copy of the todos array, updating that, and setting it as the new array.
Here is a working example: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
title: "take out trash",
done: false
},
{
id: 2,
title: "wife to dinner",
done: false
},
{
id: 3,
title: "make react app",
done: false
}
]);
const toggleDone = (e, item) => {
const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
const updatedTodos = [...todos]; // creates a copy of the array
updatedTodos[indexToUpdate].done = !item.done;
setTodos(updatedTodos);
};

What is the problem in this redux action and reducer?

I know that redux trigger react component to re-render once state of that component get changed but this doesn't happen in my situation.
Action
const addToCart = (product, cartProducts) => {
let newCartProducts = [...cartProducts, product];
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeated = acc.find(p => p.id === cur.id);
if(repeated) repeated.quantity++;
else acc.push(cur);
return acc;
}, []);
return {
type: ADD_TO_CART,
payload: newCartProducts
}
}
Reducer:
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
return action.payload;
default:
return state;
}
}
The reducer returns a new state every time the action dispatched from the component but i need to close the cart and open it again to get the effect, redux does not update the product quantity simultaneously??
You are modifying the existing elements in the state.
Use
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeatedIndex = acc.findIndex(p => p.id === cur.id);
const repeated = acc[repeatedIndex];
if (repeated) {
acc[repeatedIndex] = {
...repeated,
quantity: repeated.quantity + 1
};
} else acc.push(cur);
return acc;
}, []);
You array is recreated each time, but the objects inside it are not. So when you modify their internals you need to notify that the specific object has changed.
Refactor logic to the reducer and set the quantity here:
const addToCart = product => {
return {
type: ADD_TO_CART,
payload: product,
};
};
//I assume state is an array of products on your cart
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
const { id } = action.payload;
return state.map(p => p.id).includes(id)
? //product is already on card add quanity
state.map(p =>
p.id === id
? { ...p, quantity: p.quantity + 1 }
: p
)
: state.concat({ ...action.payload, quantity: 1 }); // add product
default:
return state;
}
};

React-select outputting a malformed array in a React.js Redux Firebase application

I've got a React Redux application that is using the npm package react-select with a multi-select piece. My problem is that it's creating a malformed array that I'm unable to iterate over.
this.state.typeOfFruit = [
0: {label: "label 1", value: "apple"},
1: {label: "label 2", value: "orange"},
2: {label: "label 3", value: "banana"}
]
I want to be able to iterate over it with a basic command right now to start gathering the right infromation.
typeOfFruit.map((fruit) => console.log(fruit.value))
I was trying to figure out how to access the malformed array or change the array into something I could access, but thought my question needed to change to how do I use this technology to create a good array.
The code below is a piece of the overall code, but it should cover all the pieces of the issue. If you see any glaring holes, please let me know.
class FruitOrderForm extends Component {
constructor(props) {
super(props)
this.state = {
typeOfFuit: [],
}
}
const fruitOptions = [
{ value: 'apple', label: 'label 1' },
{ value: 'orange', label: 'label 2' },
{ value: 'banana', label: 'label 3' },
]
handleMultiSelectChange = (typeOfFruit) => {
this.setState({ typeOfFruit });
}
onSubmit = (e) => {
e.preventDefault();
this.props.updateProfile(this.state)
document.getElementById("fruitOrderForm").reset();
}
render() {
this.state.typeOfFruit.map((fruit) => console.log(fruit.value))
return (
<div>
<form id='fruitOrderForm' onSubmit={this.onSubmit}>
< Select
id='typeOfFruit'
value={this.state.typeOfFruit}
onChange={this.handleMultiSelectChange}
options={fruitOptions }
isMulti='true'
isSearchable='true'
/>
<button>Update</button>
</form>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const profile = state.firebase.profile
return{
profile: profile,
auth: state.firebase.auth
}
}
const mapDispatchToProps = (dispatch) => {
return {
updateProfile: (users) => dispatch(updateProfile(users)),
}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([{ collection: 'users'}]))(
FruitOrderForm)
Then it goes to the store redux action
export const updateProfile = (users) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
const profile = getState().firebase.profile
const userID = getState().firebase.auth.uid;
firestore.collection('users').doc(userID).update({
...profile,
typeOfConsulting: users.typeOfConsulting
}).then(() => {
dispatch({ type: 'UPDATED_PROFILE', profile });
console.log('update successful')
}).catch((err) => {
dispatch({ type: 'UPDATE_PROFILE_ERROR', err });
console.log('There was an error', err)
})
}
}
Then through the reducer
const businessProfileReducter = (state = initState, action) => {
switch (action.type) {
case 'CREATE_BUSINESS_PROFILE':
console.log('created business profile', action.businessProfile)
return state;
case 'CREATE_BUSINES_PROFILE_ERROR':
console.log('create business profile error', action.err);
return state;
default:
return state;
}
}
export default businessProfileReducter;
Then the root reducer
const rootReducer = combineReducers({
auth: authReducer,
businessProfile: businessProfileReducer,
firestore: firestoreReducer,
firebase: firebaseReducer
})
export default rootReducer
What I want to have output by this would be a good array that I can iterate over.
this.state.typeOfFruit = [
{label: "label 1", value: "apple"}
{label: "label 2", value: "orange"}
{label: "label 3", value: "banana"}
]
It looks like you may actually be getting an object rather than an array (the fact that the keys are shown). If that is the case you can iterate over it with:
Object.keys(typeOfFruit).map((fruitId) => console.log(typeOfFruit[fruitId].value))
or
Object.values(typeOfFruit).map((fruit) => console.log(fruit.value))
Okay, #smashed-potatoes. I figured it out based on what we talked about last night. Instead of doing the Object.values where we were originally doing it, I moved it to the onSubmit function. I realized I could pull out the individual values there before it gets sent to Firebase. I added a new array to this.state and then am doing a few operations in the onSubmit function to move it to the proper this.state.typeOfConsulting array. See code below. It isn't exactly pretty, but it works!
class FruitOrderForm extends Component {
constructor(props) {
super(props)
this.state = {
typeOfFuit: [],
interimTypeOfFruit: [],
}
}
const fruitOptions = [
{ value: 'apple', label: 'label 1' },
{ value: 'orange', label: 'label 2' },
{ value: 'banana', label: 'label 3' },
]
handleMultiSelectChange = (typeOfFruit) => {
this.setState({ typeOfFruit });
}
onSubmit = (e) => {
e.preventDefault();
// I'm setting typeOfFruit to an empty array because otherwise it continues to
// push the same values to the array that are already there.
this.setState({
typeOfFruit: []
})
// Then I'm pushing just the values from this.state.interimTypeOfFruit to the
// proper this.state.typeOfFruit array that I can access like a regular array
Object.values(this.state.interimTypeOfFruit).map((fruitType) => {
this.state.typeOfFruit.push(fruitType.value)
})
this.props.updateProfile(this.state)
document.getElementById("fruitOrderForm").reset();
}
render() {
this.state.typeOfFruit.map((fruit) => console.log(fruit.value))
return (
<div>
<form id='fruitOrderForm' onSubmit={this.onSubmit}>
< Select
id='typeOfFruit'
value={this.state.typeOfFruit}
onChange={this.handleMultiSelectChange}
options={fruitOptions }
isMulti='true'
isSearchable='true'
/>
<button>Update</button>
</form>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const profile = state.firebase.profile
return{
profile: profile,
auth: state.firebase.auth
}
}
const mapDispatchToProps = (dispatch) => {
return {
updateProfile: (users) => dispatch(updateProfile(users)),
}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([{ collection: 'users'}]))(
FruitOrderForm)
I can't upvote your answer as I'm not to a 15 score yet....I marked it as the correct answer, though. Thanks for your help!

Add/remove object from redux store on one click

I need to toggle somehove adding/removing object from redux store. It is like check/uncheck. I have following code:
const list = ({ id, item }) => {
const isChecked = name => items.some(item => item.name === name);
let itemClass = cx({
item: true,
selected: isChecked(name),
});
return (
<li className={itemClass}
onClick={() => click(fullItem)} key={id}>
<div className={styles.name}>
{isChecked(name) ?
(<span><i className={`fa fa-check`}></i>{name}</span>)
: (<span>{name}</span>)
}
</div>
</li>
);
}
export const click = item => ({
type: ADD_ITEM,
payload: item,
});
import {
ADD_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ITEM:
return {
...state,
items: [action.payload],
};
default:
return state;
}
};
but for now it only work for adding item to store, when I click on item when it is selected, it should remove it from the store. How can I toggle onclick removing/adding object to redux store?
You could try something like this. Changing the ADD_ITEM instead to a TOGGLE_ITEM where you check for existence of the item using something like Array.prototype.find. Adding if it does not exist, and removing it if it does exist:
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
const currentItem = state.items.find(item => item.id === action.payload.id);
if (!currentItem) {
return {
...state,
items: [...state.items, action.payload],
};
} else {
const newItems = state.items.filter(item => item.id !== action.payload.id];
return {
...state,
items: [...newItems]
};
}
default:
return state;
}
};
You may though want to consider having separate add, update, and delete actions, and dispatch the different actions accordingly from your components.
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
// check to see if the item already in our array
// Array.some return true/false
const itemAlreadyExists = state.items.some(item => item.id === action.payload.id)
return {
...state,
// if the item already in our array filter it
// if not just add it
items: itemAlreadyExists
? state.items.filter(item => item.id !== action.payload.id)
: [...state.items, action.payload],
};
default:
return state;
}
};

return new state, don't mutate it. Am i doing it right way?

I am rendering a tabList, onclick of any tab it will be highlighted. I am achieving this by checking a selected attribute.
Everytime a tab is clicked I am changing selected to true/false in my reducer.
myTabsReducer should not mutate but return a new array.
Am I doing it right way? As per documentation We should not mutate state (should not alter state). In my case I'm altering my reducer. Is it Ok to do it or is there any other way to achieve it?
export const myTabsReducer = (id) => {
return [
{
id: 1,
name:"My Tab 1",
selected: id==1?true:false
},
{
id: 2,
name:"My Tab 2",
selected: id==2?true:false
},
{
id: 3,
name:"My Tab 3",
selected: id==3?true:false
}
]
}
const myActiveTabReducer = (state = {}, action) => {
switch (action.type) {
case SELECT_PLAN_TAB:
return action.payload
default:
return 1;
}
}
const allReducers = (state = {}, action) => {
return {
allTabs: myTabsReducer(myActiveTabReducer(state, action)),
}
}
It would be much better to have your state in this way:
const initialState = {
tabs: [
{ id: 1, name: 'Tab 1' },
{ id: 2, name: 'Tab 2' }
],
selectedTab: 1
}
so that your reducer changes only the selectedTab property on "SELECT_PLAN_TAB":
const activeTabReducer = (state = initialState, action) => {
switch (action.type) {
case SELECT_PLAN_TAB:
return {
...state,
selectedTab: action.payload
};
default:
return state;
}
}
Then, if you need the tabs array with the selected property for each tab, I would use a selector: see http://redux.js.org/docs/recipes/ComputingDerivedData.html
You are not mutating the array. Every time reducer is called you generate a new array.
const foo1 = allReducers({}, {type:' SELECT_PLAN_TAB', payload: 1})
const foo2 = allReducers({}, {type:' SELECT_PLAN_TAB', payload: 1})
console.log(foo1.allTabs === foo2.allTabs) // false
You should be careful, because this function will always return new instance even if input stays the same. This means that shallow equality check will fail and depending on how you consume this store, your component will always re-render.

Categories