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
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.
How can I remove a specific item (by id) from localstorage using react (redux - persist)? handleSubmit is working fine, but handleDelete, is not. I have this:
handleSubmit = event => {
event.preventDefault();
this.props.addWeather(this.state.weatherCity);
this.setState({ weatherCity: "" });
};
handleDelete = (event, id) => {
this.props.deleteWeather(this.state.weatherCity);
this.setState({ weatherCity: "" });
}
const mapStateToProps = state => ({
allWeather: state.allWeather
});
const mapDispatchToProps = dispatch =>
bindActionCreators(WeatherActions, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(WeatherList);
And button in form to call handleDelete:
<form onSubmit={this.handleDelete}><button type="submit" id="add" onClick={this.handleDelete}>Remove City</button></form>
My localstorage:
allWeather: "[{\"id\":0.5927975642362653,\"city\":\"Toronto\"},{\"id\":0.8124764603718682,\"city\":\"Fortaleza\"},{\"id\":0.9699736666575081,\"city\":\"Porto\"},{\"id\":0.852871998478355,\"city\":\"Tokio\"},{\"id\":0.8854642571682461,\"city\":\"New York\"}]"
My reducer:
export default function allWeather(state = [], action) {
switch (action.type) {
case "ADD_WEATHER":
return [...state, { id: Math.random(), city: action.payload.city }];
case "DELETE_ITEM":
return [...state, state.weatherCity.filter((event, id) => id !== action.payload.id)];
default:
return state;
}
}
And actions:
export const deleteWeather = id => ({
type: "DELETE_ITEM",
payload: { id }
});
I appreciate any help.
Your problem is that you are using the spread operator, which copies the content of the current state first. Then you are adding the items that were returned from the filter method. So you aren't deleting but adding. To delete from an array use the filter method only, without the spread operator like that:
return state.filter( (city) => city.id !== action.payload.id )
Also the state is an array, not an object, so this is invalid state.weatherCity.
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
When I click delete I get the Can not read property 'id' of null error.
I am confused.* How to pass the id so only the component I clicked on (delete button) is removed?**
Reducer:
const notesReducer = (state = notes, action, id) => {
switch (action.type) {
case 'ADD':
return [...state, action.newItem]
case 'DELETING':
//const newState = [...state.filter((note) => note !== action.note)]
const newState = [...state]
newState.filter((note) => note.id !== action.payload.id)
//newNote.pop()
default:
return state
}
}
Action
export const add = (id) => {
return {
type: 'ADD',
}
}
export const deleting = (id) => {
return {
type: 'DELETING',
}
}
Component
<div>
{notes.map((item, index) => (
<div>
<Select></Select>
<Dialog key={uuid()}></Dialog>
<Button
key={uuid()}
onClick={deleteNote}
>
Delete
</Button>
</div>
))}
</div>
dispatch({ type: 'ADD', mynote }),
dispatch({ type: 'DELETING' })
Based on your current implementation, you need to pass note id to
dispatch({ type: 'DELETING', note.id })
Also, in reducer, you need to return modified state.
case 'DELETING':
return state.filter((note) => note.id !== id)
As an advice, you actually don't use actions you defined, because you directly dispatch with type. So, keep in mind that it's a better approach to write actions and fire them using mapDispatchToProps.
I built an app and added CRUD functionality and everything works fine except the edit functionality. The problem is when I try to edit its actually hitting the right database and updates the entry but in the react app its just force updates all the entries to particular one entry.
Update Saga :-
function* updateFeedbackSaga(action) {
try {
const updateData = yield call(api.feedback.edit, action.payload);
yield put(actions.updateFeedback(updateData));
console.log(updateData);
} catch (err) {
yield put(actions.updateFeedbackErrors(err.response.data));
}
}
Edit Reducer
import * as actionTypes from "../Actions/types";
const initialState = {
feedbacks: [],
feedback: {},
loading: false
};
export default (state = initialState, action) => {
switch (action.type) {
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: state.feedbacks.map(
feedback =>
feedback.id === action.payload.id ? action.payload : feedback
)
};
default:
return state;
}
};
Actions
//Edit and update Feedback
export const updateFeedbackRequest = newFeedbackData => ({
type: actionTypes.UPDATE_FEEDBACK_REQUEST,
payload: newFeedbackData
});
export const updateFeedback = updatedData => ({
type: actionTypes.UPDATE_FEEDBACK,
payload: updatedData
});
export const updateFeedbackErrors = errors => ({
type: actionTypes.GET_ERRORS,
payload: errors
});
That's how printing
<section className = "feedback">
<div className = "employees__table" >
<h4 className = "semi-heading" > Feedback Table < /h4>
{
FeedbackList feedbacks = {feedbacks} />
}
</div>
</section >
const mapStateToProps = state => ({
feedbackList: selectors.FeedbackSelector(state)
});
HERE ARE THE IMAGES
This is my feedbacklist
If I edit the first item then state is like this
My feedbacklist is repeating edited feedback. I don't know where i am doing wrong.
Here is my database
Here is the working code:
https://codesandbox.io/s/github/montygoldy/employee-review/tree/master/client
login: montyjatt#gmail.com
password: 12345678
do you need to loop over all feedback when you already know the updated I'd?
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks[action.payload.id]: action.payload.body
};
This will only update a single item because the ID is part of the key.
The way you have it currently the feedbacks will all be replaced by the single value that matches the ID.
If you're planning on sending multiple id's then you'll want to use the spread operator.
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: {
...state.feedbacks,
...action.payload
}
};
In this case you're spreading out the old feedback items and then using the new payload with the spread operator to overwrite only the ones with matching id's.
Of course this means the action.payload should match your feedback structure.
Ok SO I have found the fix, actually, it's my id reference in the reducer was incorrect.
correct way is
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: state.feedbacks.map(
feedback =>
feedback._id === action.payload._id ? action.payload : feedback
)
};