I have a code that loops through all the orders an updates the is_confirmed property to 1. The thing is I have to loop through all the orders find the one that matches the order id and update it.
My question is there more efficient way to do this without looping through all the objects?
export const orders = (state = [], action) => {
const { type, payload } = action;
switch (type) {
case "NEW_ORDER":
const { new_order } = payload;
const new_state = state.concat(new_order);
//console.log(new_state);
return new_state;
case "CONFIRM_ORDER":
const { index } = payload;
return state.map((order) => {
if (order.id === index) {
return { ...order, is_confirmed: 1 };
} else {
return state;
}
});
}
return state;
};
First of all, it would be best if you make your state an object
export const orders = (state = {orders : []},action)
And access your array as state.orders.
Next, never mutate a state variable, make a copy of it first
let ordersCopy= [...state.orders]
Then you can alter this array and set it to state:
ordersCopy.forEach((order) => {
if(order.id === index){
ordersCopy.splice(index,1,{...order, is_confirmed: 1})
}
return {...state, orders: ordersCopy}
And in your other case NEW_ORDER:
return {...state, orders: [...state.orders, new_order]}
I would just make a copy of the array and find the index of the matched element using findIndex. Then, update it using brackets to access the element:
case "CONFIRM_ORDER":
const { index } = payload;
const ordersCopy = [...state]
const orderIndex = ordersCopy.findIndex(order => order.id === index)
ordersCopy[orderIndex].is_confirmed = 1
return ordersCopy
Create a state with React Class or Hook to useState()
Please check here- https://reactjs.org/docs/hooks-intro.html
Related
I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like:
{ name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }
What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.
NOTE: I have learned that I could be simply handingling this with useState
function Form(props) {
const title = props.title;
const paragraph = props.paragraph;
const formBlocks = props.blocks.formBlocks
const submitEndpoint = props.blocks.submitEndpoint || "";
const action = props.blocks.action || "POST";
const formReducer = (state, e) => {
console.log("I AM RUNNING")
state.map((obj) => {
console.log("I SHOULD RETURN SOMETHING BUT I DONT")
if (obj.name === e.target.name) {
console.log("OBJ EXISTS", obj)
return {...obj, [e.target.name]:obj.value}
} else {
console.log("NO MATCH", obj)
return obj
}
});
return state
}
const [formData, setFormData] = useReducer(formReducer, []);
const [isSubmitting, setIsSubmitting] = useState(false);
=====================================================================
Where I am calling my reducer from:
<div className="form-block-wrapper">
{formBlocks.map((block, i) => {
return <FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={setFormData}
/>
})}
</div>
Issues
When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.
When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.
Solution
Using useReducer
const formReducer = (state, action) => {
switch(action.type) {
case "UPDATE":
const { name, value } = action.payload;
return state.map((obj) => obj.name === name
? { ...obj, [name]: value }
: obj
);
default:
return state;
}
};
...
const [formData, dispatch] = useReducer(formReducer, []);
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={e => dispatch({
type: "UPDATE",
payload: {...e.target}
})}
/>
);
})}
Using useState
const [formData, setFormData] = useState([]);
...
const changeHandler = e => {
const { name, value } = e.target;
setFormData(data => data.map(obj => obj.name === name
? { ...obj, [name]: value }
: obj
));
};
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={changeHandler}
/>
);
})}
I have come to understand my problem much better now and I'll update my question to reflect this.
As the user interacted with an input I needed to figure out if they had interacted with it before
If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
If they didn't I needed to add an entirely new object to my forms state[]
I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.
const handleFormInputChange = (e) => {
const { name, value, type } = e.target;
const addObjectToArray = (obj) => {
console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
setFormData(currentArray => ([...currentArray, obj]))
}
const updateObjectInArray = () => {
const updatedObject = formData.map(obj => {
if (obj.name === name) {
//If the name matches, Update the value of the input
return ({...obj, value:value})
}
else {
//if no match just return the object as is
return obj
}
})
setFormData(updatedObject)
}
//Check if the user has already interacted with this input
if (formData.find(input => input.name === name)) {
updateObjectInArray()
}
else {
addObjectToArray({name, value, type})
}
}
I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.
I'm currently learning useReducer and try to convert useState to useReducer in todo app. My problem :
TOGGLE_TODO can't update the value when click.
// First try
case TOGGLE_TODO:
let targetId = todoItem[action.index];
let newTodo = [...todoItem];
return (newTodo[targetId].completed = !newTodo[targetId].completed);
// Second try
case TOGGLE_TODO:
return todoItem.map((todo, index) => {
if (index === action.index) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
<button
value={index}
onClick={(event) =>
dispatch({
type: TOGGLE_TODO,
index: event.target.value,
})
}
>
{todo.completed ? "done" : "pending"}
</button>
UPDATE_TODO, I have no clue for convert this to useReducer. Can I convert this too ? Here is my code using useState.
const onUpdate = (e) => {
const target = e.currentTarget.value;
const todoTarget = todoItem[target].name;
setInput(todoTarget);
setTodoIndex(target);
};
And here is my codesandbox for full code. MY CODE USING USESTATE and MY CODE USING USEREDUCER
First, you first TOGGLE_TODO handler if flawed -
// First try
case TOGGLE_TODO:
let targetId = todoItem[action.index];
let newTodo = [...todoItem];
return (newTodo[targetId].completed = !newTodo[targetId].completed);
Your todoAction is actually a reducer and not an action, so you should rename it to todoReducer. Also, a reducer needs to return a new copy of the entire state, and not just change one part of it, so your second try is correct.
case TOGGLE_TODO:
return todoItem.map((todo, index) => {
if (index === action.index) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
Notice how in the first case you are returning one todoItem, where in the second case you are returning an entire new array.
The problem with the code is that in your button, when you dispatch the action with the value, you are dispatching a string -
<button
value={index}
onClick={(event) =>
dispatch({
type: TOGGLE_TODO,
index: event.target.value, // <- This is a string
})
}
>
{todo.completed ? "done" : "pending"}
</button>
And in your reducer you are trying to compare it to a number -
if (index === action.index) // This will always be false since index is a .number and action.index is a string
The solution is to dispatch a number like so -
dispatch({
type: TOGGLE_TODO,
index: Number(event.target.value),
})
working codesandbox - https://codesandbox.io/s/long-monad-5yzjv7?file=/src/App.js
I have a todo app in JS with the following functions:
This is part of a function that passes an id into an event listener to remove a todo
removeButton.addEventListener('click', function () {
removeTodo(todo.id)
renderTodos(todos, filters)
})
This function removes the todo - I've used 2 approaches, the findIndex way works great, it removes the todo and renders the new todos fine - I thought the filter approach I've commented would also work but it doesn't, it does remove the todo but it doesn't automatically update the list in the browser unless I refresh the page, while splice does it automatically, why could this happen? could it be waiting to update local storage before renderTodos starts reading the list? Just a note that in the example that didn't work I was passing newTodos into the save function, I just changed it to todos for the splice way.
const removeTodo = function (id) {
const todoIndex = todos.findIndex(function (todo) {
return todo.id === id
})
if (todoIndex > -1) {
todos.splice(todoIndex, 1)
}
// newTodos = todos.filter(function (todo) {
// return todo.id !== id
// })
saveTodos(todos)
}
the todo list is saved in local storage
const saveTodos = function (todos) {
localStorage.setItem('todos', JSON.stringify(todos))
}
Here is the render function for information
const renderTodos = function (todos, filters) {
const filteredTodos = todos.filter(function (todo) {
const searchTextMatch = todo.text.toLowerCase().includes(filters.searchText)
const hideCompletedMatch = !filters.hideCompleted || !todo.completed
return searchTextMatch && hideCompletedMatch
})
const todosLeft = filteredTodos.filter(function (todo) {
return !todo.completed
})
document.querySelector('#todos').innerHTML = ''
document.querySelector('#todos').appendChild(generateSummaryDom(todosLeft))
filteredTodos.forEach(function (todo) {
document.querySelector('#todos').appendChild(generateTodoDom(todo))
})
}
splice() mutates the todos array which you are then renderering, while filter() returns a new array which you are not making use of.
To make it work with filter() you will need to return the newTodos from the remove function and render the returned array, not the original todos array.
removeButton.addEventListener('click', function () {
const newTodos = removeTodo(todo.id);
saveTodos(newTodos)
renderTodos(newTodos, filters);
})
const removeTodo = function (id) {
return todos.filter(todo => todo.id !== id)
}
const saveTodos = function (todos) {
localStorage.setItem('todos', JSON.stringify(todos))
}
Reassigning a variable does not have side-effects; reassigning one identifier has no effect on identifiers elsewhere. Doing
newTodos = todos.filter(function (todo) {
return todo.id !== id
})
saveTodos(todos)
}
means that you've put some results into newTodos without doing anything else with it. It doesn't get put into storage (or rendered, though how you render isn't shown).
Pass along the new filtered todos, and render (however you're doing it) from there - and don't forget to declare your variables.
const newTodos = todos.filter(function (todo) {
return todo.id !== id
})
saveTodos(newTodos);
renderTodos(newTodos);
while taking renderTodos out of the immediate listener callback.
I am trying to fetch data in localstorage using ReactJS. Can some one please help me here is my sample code.
let [rows,setRows] = useState([]);
React.useEffect(() => {
rows = localStorage.getItem("my_tier_list");
if(rows){
setRows(JSON.parse(rows));
}
},[]);
React.useEffect(() => {
localStorage.setItem("my_tier_list", JSON.stringify(cart));
});
Can some one please help me and thanks in advance
There are three problems to your above code.
You can't directly assign values to your state variable using =, you must do it using the setter functions.
You have not added the dependency list in the second useEffect.
You have not used the correct name to set the localStorage.
let [rows,setRows] = useState([]);
React.useEffect(() => {
// you can't directly set a state variable. Create a new local variable
const localRows = localStorage.getItem("my_tier_list");
if(localRows){
setRows(JSON.parse(localRows));
}
},[]);
React.useEffect(() => {
localStorage.setItem("my_tier_list", JSON.stringify(rows)); // corrected it to rows
}, [rows]); // added the array as dependency list. This will trigger this only when "rows" gets changed
Update
Based on your code shared through code sandbox, you need to update your Reducer.js.
const updateLocalStorage = (cart) => {
localStorage.setItem("my_tier_list", JSON.stringify(cart));
};
export const cartReducer = (state, action) => {
switch (action.type) {
case "ADD_TO_CART": {
const updatedState = {
...state,
cart: [...state.cart, { ...action.payload, qty: 1 }]
};
updateLocalStorage(updatedState.cart);
return updatedState;
}
case "REMOVE_FROM_CART": {
const updatedState = {
...state,
cart: state.cart.filter((c) => c.id !== action.payload.id)
};
updateLocalStorage(updatedState.cart);
return updatedState;
}
case "CHANGE_CART_QTY": {
const updatedState = {
...state,
cart: state.cart.filter((c) =>
c.id === action.payload.id ? (c.qty = action.payload.qty) : c.qty
)
};
updateLocalStorage(updatedState.cart);
return updatedState;
}
default:
return state;
}
};
And in Header.js
let [rows,setRows] = useState([]);
React.useEffect(() => {
const localRows = localStorage.getItem("my_tier_list");
if(localRows){
setRows(JSON.parse(localRows));
}
},[cart]); // adding cart will ensure any changes you make is reflected.
Please look into following sandbox: https://codesandbox.io/s/heuristic-rain-n97hf3
Set local storage item on some event handler: localStorage.setItem("value", value);
Get local storage item with: const localStorageValue = localStorage.getItem("value");
I am using redux to put products in a cart for a react native project. At the moment it's functional, but I can add duplicate items. I want to prevent that.
What's the best way to modify the reducer that will stop storing duplicates?
My Reducer:
const cartItems = (state = [], action) => {
//action type are the constatns
switch (action.type) {
case ADD_TO_CART:
// TODO: dont add duplicates
return [...state, action.payload];
case REMOVE_TO_CART:
//filter through the single item that matches payload and remove it
return state.filter(cartItem => cartItem !== action.payload);
case CLEAR_TO_CART:
//return empty state which clears cart
return (state = []);
}
//return the state
return state;
};
My action:
export const addToCart = (payload) => {
return {
type: ADD_TO_CART,
payload,
}
}
Use find to check to see if an object with that product ID exists in state. If it does return the state otherwise return the updated state.
const { product_id } = action.payload;
const dupe = state.find(obj => obj.product_id === product_id);
return dupe ? state : [...state, action.payload ];
You can add some code before doing something like:
return {...state, cart: [...state.cart].push(payload)}
. for example:
const lookForCart = state?.cart?.find(crt => crt?.cardId === payload?.cardId)
if (lookForCart) return state
return {...state, cart: [...state.cart].push(payload)}
you must check duplicate first before call action add_cart
case 1: if not has exists => push in array redux store
case 2: if has item => consider change property example increase number quantity product
You should filter out the product if it is in the Store and add the new action.payload
This will ensure that payload quantity, price, total, quantity is updated
Code:
case ADD_TO_CART:
// TODO: dont add duplicates
return [...state.filter(p => p.id !== action.payload.product_id), action.payload];