Am I updating 'fruits' properly? I would think fruits.push('something') would trigger the useEffect hook? or using setFruits would cause a re-render?
const Fruits = () => {
const [fruitInput, setFruitInput] = useState("")
const [fruits, setFruits] = useState(['Apple', 'Orange'])
const addFruit = fruit => {
console.log('addFruit', fruit)
fruits.push(fruit)
setFruits(fruits)
}
useEffect(() => {
console.log('fruits was updated', fruits)
}, [fruits])
return (
<>
<ul>{fruits.map((fruit, index) => <li key={index}>{fruit}</li>)}</ul>
<input type="text" onChange={ e => setFruitInput(e.target.value) } value={fruitInput} />
<button onClick={() => addFruit(fruitInput)}>Add Fruit</button>
</>
)
}
You're mutating the fruits array in place, so React doesn't see its changes and does not trigger a render. Add the item to newly created array instead:
const addFruit = fruit => {
console.log('addFruit', fruit)
setFruits([...fruits, fruit])
}
You have to use setFruits properly. Refactor
console.log('addFruit', fruit)
fruits.push(fruit)
setFruits(fruits)
}
to
const addFruit = fruit => {
console.log('addFruit', fruit)
setFruits(fruits=>([...fruits, fruit]))
}
Below implementation should resolve your issue:-
const Fruits = () => {
const [fruitInput, setFruitInput] = useState("");
const [fruits, setFruits] = useState(["Apple", "Orange"]);
const addFruit = () => {
const newFruits = [...fruits];
newFruits.push(fruitInput);
setFruits(newFruits);
console.log(newFruits);
};
useEffect(() => {
console.log("fruits was updated", fruits);
}, [fruits]);
return (
<>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
<input
type="text"
onChange={e => setFruitInput(e.target.value)}
value={fruitInput}
/>
<button onClick={addFruit}>Add Fruit</button>
</>
);
};
Explanation:
When you use [fruits] in useEffect then it does deep comparison, so you have to ensure that the new value in fruits is different than what was there earlier. Simply pushing value in array won't trigger the update.
Related
I am working on a react-redux project. My problem is that I cannot filter the items that come from an API according to the user input. I used reselect library but did not work.
Here is my SearchComponent:
function SearchComponent({ onClose, isOpen }) {
const dispatch = useDispatch()
const searchAnimes = useSelector(inputItems)
const filtered = createSelector(inputItems, (items, e) => {
const filterText = e.target.value.toLowerCase()
const filterWords = filterText.split("")
items.filter((item) => {
return filterWords.every((word) =>
item.title.toLowerCase().includes(word)
)
})
})
useEffect(() => {
dispatch(fetchInputData())
}, [dispatch])
return (
"... Some code"
<input
type="text"
placeholder="You can search for `Kyoukai no Kanata` for example"
onChange={filtered}
/>
{searchAnimes.map((el, id) => (
<div className="searchInput" key={id}>
<img src={el.images.jpg.small_image_url} alt="" />
<p>{el.title}</p>
</div>
))}
</div>
</Modal>
)
}
Here is my slice:
export const fetchInputData = createAsyncThunk(
"anime/fetchInputData",
async () => {
const response = await axios.get(`${process.env.REACT_APP_API_KEY}?limit=5`)
// console.log(response.data.data)
return response.data.data
}
)
Btw, there is no problem with selector. Thanks in advance!
const filterWords = filterText.split("")
will break up your string by characters, not by words. I believe you want:
const filterWords = filterText.split(" ")
const Component = ()=>{
const [list, setList] = useState(getLocalStorage());
const [isEditing, setIsEditing] = useState(false);
const [itemToEdit, setItemToEdit] = useState();
const refContainer = useRef(null);
const putLocalStorage = () => {
localStorage.setItem("list", JSON.stringify(list));
};
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
return list.find((item) => item.id === id);
});
setIsEditing(true);
};
const handleSubmit = (e)=>{
e.preventDefault();
let nameValue = refContainer.current.value;
if (isEditing){
setList(list.map((item)=>{
if (item.id === itemToEdit.id){
return {...item, name: nameValue};
}
else {
return item;
}
);
}
else {
let newItem = {
id: new Date().getItem().toString(),
name: nameValue,
}
setList([...list, newItem])
}
nameValue="";
setIsEditing(false);
}
useEffect(() => {
putLocalStorage();
}, [list]);
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""}/>
<button type="submit">submit</button>
</form>
<div>
{list.map((item) => {
const { id, name } = item;
return (
<div>
<h2>{name}</h2>
<button onClick={() => editItem(id)}>edit</button>
<button onClick={() => deleteItem(id)}>
delete
</button>
</div>
);
})}
</div>
</div>
)
}
So this part:
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""} />
I want to show to users what they are editing by displaying the itemToEdit on the input.
It works on the first time when the user clicks edit button
But after that, the defaultValue does not change to itemToEdit
Do you guys have any idea for the solution?
(i could use controlled input instead, but i want to try it with useRef only)
Otherwise, placeholder will be the only solution...
The defaultValue property only works for inicial rendering, that is the reason that your desired behavior works one time and then stops. See a similar question here: React input defaultValue doesn't update with state
One possible solution still using refs is to set the itemToEdit name directly into the input value using ref.current.value.
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
const item = list.find((item) => item.id === id);
refContainer.current.value = item.name;
return item;
});
setIsEditing(true);
};
Im a newbie in React and Im creating a simple form that sends data to DB. I made it work almost as I wanted, the only problem is that I dont know how to update the state which has an array inside.
The idea is to make a form so I can add recipes which include the whole recipe data that I map through to render each recipe. In the data object I need simple strings most of the time but then I need also three arrays or objects, I prefer the arrays in this case.
I found many solutions for class components but still I could figure out how to update the arrays. I even figured out how to update one array from a string input separated only with commas, then .split(', ') and .trim() and map() through but I could not setFormFields({}) at two places at the same time since the createRecipe() is async. The split just did not happen before the array was sent to the DB as a string. Thats why I dont put the whole code here.
I will simplify the code to make you see clear.
const defaultFormFields = {
title: '',
imageUrl: '',
leadText: '',
};
const NewRecipeForm = () => {
const [formFields, setFormFields] = useState(defaultFormFields);
const { title, imageUrl, leadText } = formFields;
const [ingredients, setIngredients] = useState([])
const handleFormFieldsChange = (event) => {
setFormFields({ ...formFields, [event.target.name]: event.target.value })
}
const handleIngredientsChange = ( event) => {
**// here I need help**
setIngredients()
}
const addIngredient = () => {
setIngredients([...ingredients, ''])
}
const removeIngredient = (index) => {
**// here I need help**
}
const createRecipe = async (event) => {
event.preventDefault()
// addRecipe sends the object to Firestore DB
addRecipe('recipes', url, formFields)
resetFormFields()
}
const resetFormFields = () => {
setFormFields(defaultFormFields);
};
return (
<NewRecipeFormContainer>
<h1>New recipe</h1>
<form onSubmit={createRecipe}>
<h1>Title</h1>
<input label='Title' placeholder='Recipe title' name='title' value={title} onChange={handleChange} />
<input label='imageUrl' placeholder='imageUrl' name='imageUrl' value={imageUrl} onChange={handleFormFieldsChange} />
<input label='leadText' placeholder='leadText' name='leadText' value={leadText} onChange={handleFormFieldsChange} />
<h1>Ingredients</h1>
**// here I need help ?**
{
ingredients.map((ingredient, index) => {
return (
<div key={index}>
<input label='Ingredience' placeholder='Ingredience' name='ingredient' value={ingredient.ingredient} onChange={handleChange} />
**// here I need help ?**
<button onClick={removeIngredient} >remove</button>
</div>
)
})
}
<button onClick={addIngredient} >add</button>
</form>
<Button onClick={createRecipe}>ODESLAT</Button>
</NewRecipeFormContainer>
)
}
I will appreciate any hint or help. Ive been totally stuck for two days. Thank you!
Here's an example of how to update a single element in a list.
const updateSingleItemInList = (index, update) => {
setList(list.map((l, i) => i === index ? update : l));
};
const add = (element) => setList([...list, element]);
Try simplifying your state first:
const [ingredients, setIngredients] = useState([]);
const [tips, setTips] = useState([]);
Then it becomes simple to write the handlers:
const updateIngredient = (index, text) => {
setIngredients(list.map((ing, i) => i === index ? text : ing));
};
const addIngredient = () => setIngredients([...ingredients, ""]);
Then you can create the form object when the user wants to submit:
addRecipe('recipes', url, {
ingredients: ingredients.map(i => ({ingredients: i})),
// etc.
});
Put it all together and here is the minimum viable example of a component that manages a dynamic number of form elements (tested, works):
export const TextBody = () => {
const [list, setList] = useState([{ name: "anything" }]);
const add = () => setList(l => [...l, { name: "" }]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text) => setList(l => l.map((ll, ii) => (ii === i ? { name: text } : ll)));
return (
<>
<TouchableOpacity onPress={add}>
<Text text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<>
<Text text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<Text text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text)} />
</>
))}
</>
);
};
You can return those CRUD functions and the state from a custom hook so you only have to write this once in a codebase.
Edit: Just for fun, here's the same component with a reusable hook:
const useListOfObjects = (emptyObject = {}, initialState = []) => {
const [list, setList] = useState(initialState);
const add = () => setList(l => [...l, emptyObject]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text, field) =>
setList(l => l.map((ll, ii) => (ii === i ? { ...ll, [field]: text } : ll)));
return {
list,
add,
remove,
update,
};
};
export const TextBody = () => {
const { list, add, remove, update } = useListOfObjects({ name: "", id: Math.random() });
return (
<>
<TouchableOpacity onPress={add}>
<TextBlockWithShowMore text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<React.Fragment key={`${l.id}`}>
<TextBlockWithShowMore text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<TextBlockWithShowMore text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text, "name")} />
</React.Fragment>
))}
</>
);
};
I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.
I want to get latest state after updating state.
So I need to use useEffect.
After I change content of todo, I call saveEditedTodo onBlur.
So my code is,
useEffect(() => {
console.log(todos)
// I need to setTodos(todos), but it causes infinite loop
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
And onBlur from props is,
const handleOnBlurTodo = (value) => {
const newValue = convertTodoToNote(value);
setEditableNote({ ...editableNote, content: newValue });
};
How can I get latest state using useEffect?
(+) Here is my full code!
function TodoList({ todoContent, onBlur }) {
const [todos, setTodos] = useState(todoContent);
const [isHover, setIsHover] = useState({ hoverID: '', onHover: false });
const { hoverID, onHover } = isHover;
const isEditable = useSelector((state) => state.isSelected);
const doneTodo = todos ? todos.filter((todo) => todo.isDone).length : 0;
useEffect(() => {
console.log(todos);
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
const handleDeleteTodo = (id) => {
let newTodos = todos.filter((el) => el.id !== id);
setTodos(newTodos);
onBlur(todos);
};
const handleOnMouseOver = (id) => {
setIsHover({ hoverID: id, onHover: true });
};
const handleOnMouseLeave = (id) => {
setIsHover({ hoverID: id, onHover: false });
};
const handleCheckbox = (id) => {
const newTodos = todos.map((todo) =>
todo.id === id ? { ...todo, isDone: !todo.isDone } : todo,
);
setTodos(newTodos);
console.log('[todos]' + todos);
};
const todoTask = todos.filter((todo) => !todo.isDone);
const doneTask = todos.filter((todo) => todo.isDone);
if (isEditable && todos) {
let todoList = todoTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
checked={todo.isDone}
onChange={() => handleCheckbox(todo.id)}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onBlur={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
let doneList = doneTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
onBlur={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onInput={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
return (todoList = (
<div>
{todoList}
{doneTodo > 0 && <CompletedTodo doneTodo={doneTodo} />}
{doneList}
</div>
));
}
if (!isEditable && todos) {
const todoList = todos.map((todo, i) => (
<TodoListContainer key={i}>
<Checkbox
type="checkbox"
onChange={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle size="small">{todo.todoItem}</NoteTitle>
</TodoListContainer>
));
return todoList;
}
return null;
}
export default TodoList;
Generally React.useEffect() is used for performing side effects for a React component. What I believe is that you wish to get the new state rendered on screen after saving the TODO content, and that can be just achieved by an onChange handler wherever you are receiving the input for your todos.
<TextField onChange={(e) => saveEditedTodos(e, id)} />
This will trigger the saveEditedTodos callback every time the value of the TextField changes. If you want to trigger the callback on clicking a save button, you can add an onClick handler in the Button component.
Another scenario what I can imagine is that you're saving your TODOs somewhere, so you want to update the list on the screen after saving the TODO in some storage, in that case you can fetch the value of todoList on each save. This can be done inside a useEffect hook callback.
React.useEffect(() => {
fetchTodos().then((response) => setTodos(response.data))
})
Here fetchTodos() is a JS Promise or async function which fetches the updated state of TODOs and sets the received data using setTodos