I have a problem where useState updates the state but does not show the changes until I refresh the app. First, I declare an array called sampleFriends, made up of objects with fields "name", "location", and "picture" (each element of the array looks similar to: {name: 'John', location: 'Boston', picture: TestImage}).
Then, I have the following useState:
const [selectedFriends, setSelectedFriends] = useState([])
At some point, I successfully render
sampleFriends.map(({ name, location, image }, index) => (
<NewMsgTableRow
name={name}
index={index}
location={location}
image={image}
onPress={() => selectFriend(name)}
/>
))
And I also have this function right above
const selectFriend = name => {
// if the friend is not already selected
if (!selectedFriends.find(e => e === name)) {
const newFriends = selectedFriends
newFriends.push(name)
setSelectedFriends(newFriends)
}
}
The component NewMsgTableRow has a button that uses onPress
<TouchableOpacity
onPress={onPress}
>
So, I want to render selectedFriends as soon as they are selected (the TouchableOpacity is touched and thus the state updates). However, when I click the button, nothing shows up until I edit and save my code and it refreshes automatically. It was my understanding that useState rerendered the components as soon as it was updated, but it is not happening in this case and I can't figure out why. I've been reading that it is async and that it does not change it instantly, but I don't know how to make it work. Hope it makes sense and thanks for your help!!
You can use array spread or Array.concat() to make a shallow clone, and add new items as well) so change the line below:
const newFriends = selectedFriends
to this line :
const newFriends = [...selectedFriends]
Related
I'm stuck at a problem - I'm building a receipes app with Firebase Realtime. I've a working prototype, but I'm stuck with an issue where useEffect won't trigger a reload after editing the array [presentIngredients].
This how my presentIngredients is defined (note that presentIngredient is used to store the current user input before the user adds the ingredient. After that, the presentIngredient get's added to the presentIngredients!):
const [ presentIngredients, setPresentIngredients ] = useState([]);
const [ presentIngredient, setPresentIngredient ] = useState('');
My useEffect hook looks like that:
useEffect(() => {
console.log('useEffect called!')
return onValue(ref(db, databasePath), querySnapshot => {
let data = querySnapshot.val() || {};
let receipeItems = {...data};
setReceipes(receipeItems);
setPresentIngredients(presentIngredients);
})
}, [])
Here's my code to render the UI for adding/removing existing ingredients:
{ /* adding a to-do list for the ingredients */ }
<View style={styles.ingredientsWrapper}>
{presentIngredients.length > 0 ? (
presentIngredients.map((key, value) => (
<View style={styles.addIngredient} key={value}>
<Text>{key} + {value}</Text>
<TouchableOpacity onPress={() => removeIngredient(key)}>
<Feather name="x" size={24} color="black" />
</TouchableOpacity>
</View>
))
) : (
<Text style={styles.text}>No ingredients, add your first one.</Text>
)}
<View style={styles.addIngredientWrapper}>
<TextInput
placeholder='Add ingredients...'
value={presentIngredient}
style={styles.text}
onChangeText={text => {setPresentIngredient(text)}}
onSubmitEditing={addIngredient} />
<TouchableOpacity onPress={() => addIngredient()}>
<Feather name="plus" size={20} color="black" />
</TouchableOpacity>
</View>
</View>
And this is my function to delete the selected entry from my presentIngredients array or add one:
// update the ingredients array after each input
function addIngredient() {
Keyboard.dismiss();
setPresentIngredients(presentIngredients => [...presentIngredients, presentIngredient]);
setPresentIngredient('');
}
// remove items by their key
function removeIngredient(id) {
Keyboard.dismiss();
// splice (remove) the 1st element after the id
presentIngredients.splice(id, 1);
setPresentIngredients(presentIngredients);
}
The useEffect hook isn't triggered when adding an ingredient, however the change is instantly rendered on the screen. If I delete an item, the change isn't noticeable until I reload the screen - what am I doing wrong?
Note that all this is happening before data is send to Firebase.
Issues
There are a few overt issues I see with the code:
The code uses the array index as the React key, so if you mutate the array, i.e. add, remove, reorder, etc... the index values won't be the same as they were on a previous render cycle per array element. In other words, the React key is the same regardless what value is now at any given index and React likely bails on rerendering.
The removeIngredient callback handler is mutating the existing state instead of creating a new array reference.
Solution
Using the array index as a React key is bad if you are actively mutating the array. You want to use React keys that are intrinsically related to the data so it's "sticky" and remains with the data, not the position in the array being mapped. GUIDs and other object properties that provide sufficient uniqueness within the data set are great candidates.
presentIngredients.map((el, index) => (
<View style={styles.addIngredient} key={el.id}>
<Text>{el.key} + {index}</Text>
<TouchableOpacity onPress={() => removeIngredient(el.id)}>
<Feather name="x" size={24} color="black" />
</TouchableOpacity>
</View>
));
Use Array.prototype.filter to remove an element from an array and return a new array reference.
function removeIngredient(id) {
Keyboard.dismiss();
setPresentIngredients(presentIngredients => presentIngredients.filter(
el => el.id !== id
));
}
Note above that I'm assuming the presentIngredients data element objects have a GUID named id.
Listening for state updates
The useEffect hook isn't triggered when adding an ingredient, however
the change is instantly rendered on the screen.
The single useEffect only exists to run once when the component mounts to fetch the data and populate the local state. If you want to then issue side-effects when the state updates later you'll need a second useEffect hook with a dependency on the state to issue the side-effect.
Example:
useEffect(() => {
console.log("presentIngredients updated", { presentIngredients });
// handle side-effect like updating the fire store
}, [presentIngredients]);
you need to create another useEffect, because the first useEffect need to be called only once (because you are setting a watcher).
useEffect(() => {
// this effect will be executed when presentIngredients change
}, [presentIngredients]);
if you don't remember how the array of dependencies work
you can check the explanation here
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
I have a diary object with 2 meals
function Magicdiary() {
const [diary, setDiary] = React.useState<MagicDiaryDay[]>([
{ mealName: "Breakfast", ingredient: null },
{ mealName: "Lunch", ingredient: null },
]);
return (
<div>
<p>meal 1: {diary[0].ingredient?.productName}</p>
<Button onClick={() => console.log(diary[0].ingredient?.productName)}>
log diary
</Button>
{diary.map((meal, index) => {
return (
<MealComponentForMagicDiary
diary={diary}
setDiary={setDiary}
index={index}
/>
);
})}
</div>
);
}
I have selection of ingredients that I call from my backend, and everytime I select current ingredient, I set it to the diary:
// MealComponentForMagicDiary
useEffect(() => {
if (hit) {
const diaryCopy = diary;
diaryCopy[index].ingredient = {
productName: hit.productName,
nutrients: {
"energy-kcal_serving": hit.calories,
protein_serving: hit.protein,
carbohydrates_serving: hit.carbs,
fat_serving: hit.fats,
},
};
setDiary(diaryCopy);
}
}, [hit, selectedHit]);
As you can see meal 1 is empty, but when I log it on the console I can see the correct productName what is the cause of this bug?
You are updating the state in the wrong way, you are mutating the original array that is overwriting the exiting array, Instead, you need to do it in an immutable way that is providing a new Instance of diary whenever you want to update it, you can do in the following way
useEffect(() => {
if (hit) {
const diaryCopy = diary.map((d, ind) => {
if (ind === index) {
// The Diary ingredient you want to update
d.ingredient = {
productName: hit.productName,
nutrients: {
"energy-kcal_serving": hit.calories,
protein_serving: hit.protein,
carbohydrates_serving: hit.carbs,
fat_serving: hit.fats,
}
};
}
return d;
} );
setDiary(diaryCopy);
}
}, [hit, selectedHit]);
From my limited react experience, when funny things like this happen I have a few go-to methods to try. One of them is to call an empty function when passing a function down as a prop. I.e. instead of:
<MealComponentForMagicDiary
diary={diary}
setDiary={setDiary}
index={index}
/>
try:
<MealComponentForMagicDiary
diary={diary}
setDiary={() => setDiary}
index={index}
/>
I'd love to know why this works sometimes, if anybody (does anybody?) understands react properly.
My fingers are crossed that it works for you!
It's hard to say exactly with this given information but I'm inclined to say this:
You're not getting the information you want because when the component renders, it's not there. Take for example this:
<p>meal 1: {diary[0].ingredient?.productName}</p>
You check if ingredient exists but are you sure diary[0] exists? Since you're setting this data elsewhere in a useEffect, I suspect that it's not available at render - even though you can console it.
I suggest using the React Developer Tools to look at your component tree and see what that state looks like when it's rendered.
That's all I can guess without more code.
I have an array of objects that looks like this:
const columns = [
{
key: "Source_campname",
title: "TS Camp Name",
customElement: function (row) {
return (
<FormControlLabel
control={
<Checkbox
checked={checkbox[row.id]}
key={row.id}
onChange={() =>
handleChange(row.Source_campname, row.id, checkbox)
}
name={row.id}
/>
}
label={[row.Source_campname]}
/>
);
}
},
{
key: "Tracker_campname",
title: "TR Camp Name"
}
];
You can see a "handleChange" function above, this is used to check/uncheck the component
The handleChange function looks like this:
const handleChange = (name, campid) => {
setCheckBox({ ...checkbox, [campid]: !checkbox[campid] });
};
You can also see a "customElement" function above. This function is rendered in another React component named ThanosTable. I will just write down part of the code where the rendering of customElement happens below.
return (
<> columnArray[0].customElement(row) </>
);
In the end you get 10 checkboxes, and you have a few pages that can be changed using pagination.
Do check my codesandbox link here for a working example:
https://codesandbox.io/s/magical-germain-8tclq
Now I have two problems:
Problem 1) If I select a few checkboxes, then go to second page and return, the checkbox state is empty and the original checkboxes are unselected. No idea why that is happening. How do I prevent that?
Problem 2) The value of checkbox state is always an empty object ({}) inside customElement function. You can see this by checking console.log(checkbox) inside customElement function (Check Line 76 in codesandbox). I thought it should be an array with selected checkbox items.
The useEffect hook embodies all the lifecycle events of a component. Therefore if you try to set checkbox in useEffect it'll infinitely update the component because updating state calls useEffect. This is probably why you see your state constantly being reset.
Instead, initialize your state with the rows before rendering.
const rows = [
...
];
let checkboxObj = {};
// if (rows) {
rows.forEach((e) => {
checkboxObj[e.id] = false;
});
const [checkbox, setCheckBox] = useState(checkboxObj);
I have data coming from getTasks() and I store it on tasks
I created a component for each task with their data with a checkbox for each task.
I want to click a checkbox and display only the one I clicked, not them all.
I am using React Native. How can I do this?
thanks.
export default () => {
const [tasks, setTasks] = React.useState([]);
const [checked, setChecked] = React.useState(false);
React.useEffect(() => {
(async () => {
const response = await getTasks();
setTasks(response);
})();
}, []);
return tasks.map(({id, title, description, date}) => (
<View key={id} style={styles.task}>
<CheckBox
value={checked}
onValueChange={() => {
setChecked((prevState) => !prevState);
}}
/>
<View style={styles.taskTextContainer}>
<Text>{title}</Text>
<Text>{description}</Text>
<Text>{date}</Text>
</View>
</View>
));
};
You're passing the same value={checked} to all checkboxes. You can create an array of booleans to accomplish that.
You have two problems in that code, first is the problem you described, the second one is the way you're changing the state of that checkbox.
About the first problem, in order to display the respective state for each task, you should also include a property that defines if the task is marked or not.
First, verify the name of the property thats set if the task is done or not, I'll give the name here as done and done is of type boolean.
// notice the done property
return tasks.map(({id, title, description, date, done}) => (
Then, you should use done to change the checked state of your Checkbox like this:
<Checkbox
value={done}
...
/>
Now you should change a little how you change that checked state.
First, you need to write a function to change the value of done, to fase or true, according by the Checkbox component's state and that's really simple:
const onCheckboxChange = (taskId) => {
// first, let's find the index of the changed task
const index = tasks.findIndex(({ id }) => id === taskId)
// next, let's get the data by the index
const data = tasks[index]
// now, we can just toggle done value like this
data.done = !data.done
// then, let's assign updated data to its own index in tasks array
tasks[index] = data
// finally, we can update the tasks state using a copy of changed tasks
setTasks([...tasks])
}
I've tried to ask this ungooglable to me question dozens of times. I've made almost the simpliest example possible to ask this question now.
I change the value of the hook in the handleChange method. But then console.log always shows previous value, not new one. Why is that?
I need to change the value of the hook and then instead of doing console.log use it to do something else. But I can't because the hook always has not what I just tried to put into it.
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You can try it here.
https://codesandbox.io/s/awesome-lumiere-y2dww?file=/src/App.js
I believe the problem is that you are logging the value in the handleChange function. Console logging the value outside of the function logs the correct value. Link: https://codesandbox.io/s/async-fast-6y71b
Hooks do not instantly update the value you want to update, as you might have expected with classes (though that wasn't guaranteed either)
State hook, when calling setValue will trigger a re-render. In that new render, the state will have the new value as you expected. That's why your console.log sees the old value.
Think of it as in each render, the state values are just local variables of that component function call. And think as the result of your render as a result of your state + props in that render call. Whenever any of those two changes (the props from your parent component; the state, from your setXXX function), a new render is triggered.
If you move out the console.log outside of the callback handler (that is, in the body of your rendered), there you will see in the render that happens after your interaction that the state is logged correctly.
In that sense, in your callbacks events from interactions, you just should worry about updating your state properly, and the next render will take care to, given the new props/state, re-render the result
The value doesn't "change" synchronously - it's even declared with a const, so even the concept of it changing inside the same scope doesn't make sense.
When changing state with hooks, the new value is seen when the component is rerendered. So, to log and do stuff with the "new value", examine it in the main body of the function:
const ControllableStates = () => {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
};
// ADD LOG HERE:
console.log('New or updated value:', value);
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You're printing out the old value in handleChange, not the new val.
i.e.
const handleChange = val => {
setValue(val);
console.log(value);
};
Should be:
const handleChange = val => {
setValue(val);
console.log(val);
};
Actually, lets get a little back and see the logic behind this scenario.
You should use the "handleChange" function ONLY to update the state hook, and let something else do the logic depends on that state hook value, which is mostly accomplished using "useEffect" hook.
You could refactor your code to look like this:
const handleChange = val => {
setValue(val);
};
React.useEffect(() => {
console.log(value);
// do your logic here
}, [value])
So I think that the main problem is that you're not understanding how React
deals with components and states.
So, I'll vastly simplify what React does.
React renders a new component and remembers it's state, it's inputs (aka
props) and it's the state and inputs of the children.
If at any given point an input changes or a state changes, React will render
the component again by calling the component function.
Consider this:
function SomeComponent(text) {
return (<div>The <i>text</i> prop has the value {text}</div>)
}
Let's say the initial prop value is "abc", React will call SomeComponent("abc"), then the function returns
<div>The <i>text</i> prop has the value abc</div> and React will render that.
If the prop text does not change, then React does nothing anymore.
Now the parent component changes the prop to "def", now React will call
SomeComponent("def") and it will return
<div>The <i>text</i> prop has the value def</div>, this is different from
last call, so React will update the DOM to reflect the change.
Now let's introduce state
function SomeComponent() {
const [name, setName] = React.useState("John")
function doSomething()
{
alert("The name is " + name)
}
return (
<p>Current name: {name}</p>
<button onClick={() => setName("Mary")}>Set name to Mary</button>
<button onClick={() => setName("James")}>Set name to James</button>
<button onClick={() => doSomething()}>Show current name</button>
)
}
So here React will call SomeComponent() and render the name John and the 3
button. Note that the value of the name variable does not change during the
current execution, because it's declared as const. This variable only reflects
the latest value of the state.
When you press the first button, setName() is executed. React will internally store
the new value for the state and because of the change of state, it will render
the component again, so SomeComponent() will be called once again. Now the variable name will
reflect again the latest value of the state (that's what useStatedoes), so in this case Mary. React
will realize that the DOM has to be updated and it prints the name Mary.
If you press the third button, it will call doSomething() which will print the
latest value of the name variable because every time React calls
SomeComponent(), the doSomething() function is created again with the latest
value of name. So once you've called setName(), you don't need to do
anything special to get the new value. React will take care of calling the
component function again.
So when you don't use class components but function components, you have to think
differently: the function gets called all the time by React and at any single
execution it reflects the latest state at that particular point in time. So when you
call the setter of a useState hook, you know that the component function will
be called again and useState will return the new value.
I recommend that you read this article, also read again Components and
Props from the React documentation.
So how should you do proceed? Well, like this:
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
const handleClick = () => {
// DOING SOMETHING WITH value
alter(`Now I'm going to do send ${value}`);
}
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
<button type="button" onClick={handleClick}>Send selected option</button>
</div>
);
}
See CodeSandbox.