For some reason I can't seem to find an answer to this rather simple task I believe. My challenge is that I have a usestate array which holds multiple objects depending on the user input. It can have as many as the user chooses or none. For some reason I can't get it to work right.
What I mean is that whenever I set the state (for example: setFinal(final => [...final, value]);) in any way that I've been able to come up with so far it doesn't work properly. It works fine when the user adds on click a new object to the array but deleting them and particularly coming from 1 to 0 to 1 again causes the state to be invalid. What I mean is that the last one before going to 0 doesn't get deleted and when you start to add new objects from 0 again it has already one value in state.
So, what is the right way to set state so that it works correctly no matter if you're adding or deleting objects from the state?
Hopefully I've made myself clear enough for a solution to this. It should be fairly straightforward but I don't seem to get it right and googling doesn't seem to do the trick. Thanks in advance for anyone helping me.
EDIT:
Something for clarification:
example objects:
0: Object { id: 484, data: [] }
1: Object { id: 524, data: [] }
2: Object { id: 170, data: (3) […] }
What I've tried so far in setting state:
setFinal(final => [...final, value]);
setFinal(value);
setFinal(final => value);
I would need it to work no matter whether you add or delete, delete them all and add again, it should work in all of these conditions.
Are you looking something like this? Live Demo
function App() {
const [arr, setArr] = useState({
numbers: [
{ id: 1, name: "Reactjs" },
{ id: 2, name: "Vuejs" },
{ id: 3, name: "Nodejs" }
]
});
const onclickHandler = event => {
setArr({
...arr,
numbers: [
...arr.numbers,
{
id: Math.floor(Math.random() * 100),
name: `${Math.random()} technology`
}
]
});
};
const deleteIt = item => {
let updated = arr.numbers.filter(a => a.id !== item.id);
setArr({ numbers: updated });
};
return (
<div className="App">
<button onClick={onclickHandler} value="4">
Add
</button>
<ul>
{arr.numbers.map(a => (
<li key={a.id}>
<span>
{a.id}-{a.name}
</span>
<button style={styles} onClick={() => deleteIt(a)}>
X
</button>
</li>
))}
</ul>
</div>
);
}
const styles = {
color: "white",
backgroundColor: "red"
};
You can't delete element from array via setFinal(final => [...final, value]); If value is undefined then setFinal just ignore it. Please see: Delete item from state array in react
Related
I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.
How can I prevent the inputs to lose focus when typing?
Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx
Here is the source code demonstrating the issue:
import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<For each={todos()}>
{(todo, index) => {
console.log('render', index(), todo)
return <div>
<input
value={todo.text}
onInput={event => {
setTodos(todos => {
return replace(todos, index(), {
...todo,
text: event.target.value
})
})
}}
/>
</div>
}}
</For>
Data: {JSON.stringify(todos())}
</div>
</div>
);
}
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
const clone = array.slice(0)
clone[index] = newItem
return clone
}
render(() => <App />, document.getElementById("app")!);
UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx
<For> components keys items of the input array by the reference.
When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.
You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.
const [todos, setTodos] = createStore([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
setTodos(o => o.id === id, "text", text)
}
Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property:
https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key
<Key each={todos()} by="id">
...
</Key>
While #thetarnav solutions work, I want to propose my own.
I would solve it by using <Index>
import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
const clone = array.slice(0);
clone[index] = newItem;
return clone;
}
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<Index each={todos()}>
{(todo, index) => {
console.log("render", index, todo());
return (
<div>
<input
value={todo().text}
onInput={(event) => {
setTodos((todos) => {
return replace(todos, index, {
...todo(),
text: event.target.value
});
});
}}
/>
</div>
);
}}
</Index>
Dat: {JSON.stringify(todos())}
</div>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline.
To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.
This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.
With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.
You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.
The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<ul>
{indexArray(todos, (todo, index) => (
<li>
<input
value={todo().text}
onInput={(event) => {
const text = event.target.value;
setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
}}
/>
</li>
))}
</ul>
);
}
Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.
I am somewhat new to React and I am running into an issue and I was hoping someone will be willing to help me understand why my method is not working.
I have this state:
const [beers, setBeers] = useState([
{
id: 8759,
uid: "8c5f86a9-87bf-41fa-bc7f-044a9faf10be",
brand: "Budweiser",
name: "Westmalle Trappist Tripel",
style: "Fruit Beer",
hop: "Liberty",
yeast: "1056 - American Ale",
malts: "Special roast",
ibu: "22 IBU",
alcohol: "7.5%",
blg: "7.7°Blg",
bought: false
},
{
id: 3459,
uid: "7fa04e27-0b6b-4053-a26b-c0b1782d31c3",
brand: "Kirin",
name: "Hercules Double IPA",
style: "Amber Hybrid Beer",
hop: "Nugget",
yeast: "2000 - Budvar Lager",
malts: "Vienna",
ibu: "18 IBU",
alcohol: "9.4%",
blg: "7.5°Blg",
bought: true
}]
I am rendering the beers with a map function and I have some jsx that calls a handleClick function
<button onClick={() => handleClick(beer.id)}>
{beer.bought ? "restock" : "buy"}
</button>
this is the function being called:
const handleClick = (id) => {
setBeers((currentBeers) =>
currentBeers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
console.log(beer);
}
return beer;
})
);
};
I wanted to use an updater function to update the state, I am directly mapping inside the setter function and since map returns a new array, I thought everything would work correctly but in fact, it doesn't. It works only on the first button click and after that it stops updating the value.
I noticed that if I use this method:
const handleClick = (id) => {
const newbeers = beers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
}
return beer;
});
setBeers(newbeers);
};
Then everything works as expected.
Can someone help me understand why my first method isn't working?
OK, I think I have figured it out. The difference between my sandbox and your sandbox is the inclusion of <StrictMode> in the Index file. Removing this fixes the issue, but is not the correct solution. So I dug a little deeper.
What we all missed was that in your code you were modifying the previous state object that is passed in. You should instead be creating a new beer object and then modifying that. So this code works (I hope):
setBeers((currentBeers) =>
currentBeers.map((currentBeer) => { // changed beer to currentBeer
const beer = {...currentBeer};
if (beer.id === id) {
beer.bought = !beer.bought;
}
return beer;
)
});
I hope that this helps.
react does not deeply compares the object in the state. Since you map over beers and just change a property, they are the same for react and no rerender will happen.
You need to set the state with a cloned object.
e.g.:
import {cloneDeep} from 'lodash';
...
setBeers(
cloneDeep(currentBeers.map((beer) => {
if (beer.id === id) {
beer.bought = !beer.bought;
console.log(beer);
}
return beer;
})
)
);
I have currently this array of objects like this:
const data = [
{
name: "jack",
number: 3,
isEnabled: false,
},
{
name: "Daniel",
number: 5,
isEnabled: false,
},
{
name: "John",
number: 6,
isEnabled: false,
},
];
Inside the render I have mapped the data array to return some divs:
{
data.map((d) => {
return (
<div onClick={() => pushClickedNumbers(d.number)}>
`${d.name}${d.number}`
</div>
);
});
}
Now I would like to create a function to push that number inside another array state, every time i click on that div, and to remove it when I click again. I've tried something like this to add, but I can't figure it out how to remove it:
const pushClickedNumber = (number) => {
setArrayOfNumbers(number, ...arrayOfNumbers);
};
The state get populated with the div clicked, but I'm struggling to find a way to remove the same number when I click again on the same div. Thank you
First of all, I would change the name of the function since it doesn't only push to the array, but also removes from it based on a condition:
{data.map(d=>{
return <div onClick={()=>handleNumberClick(d.number)}>`${d.name}${d.number}`</div>
}}
and you can do a simple check inside the function (add if not present, remove if present:
const handleNumberClick =(number)=>{
setArrayOfNumbers(nums => nums.includes(number) ? nums.filter(n => n !== number) : [number, ...nums])
}
const TestScreen = (props) => {
const [data, setData] = useState([
{ "id": "0", "user": "Lisa","amount": 1 },
]);
return(
<View style={{
flex:1,
alignItems:'center',
justifyContent:'center'
}}>
<Text>amount : {data[0].amount}</Text>
<Text>User : {data[0].user}</Text>
<Button title="update" onPress={()=>setData(??????)}></Button>
</View>
)
}
export default TestScreen;
what is the best way to add an amount number on the user Lisa? i can do
// setData([{ "id": "0", "user": "Lisa","amount": data[0].amount + 1}])
but what i have 5 users or 20?
even with a returning function nothing gets updated exept console logged witch show me the actual value
let countAddFunc =(getArr)=>{
let arr = getArr
arr[0].amount++
console.log(arr[0].amount);
return(
arr
)
}
<Button title="update" onPress={()=>setData(countAddFunc(data))}></Button>
One of the key concepts in React is Do Not Modify State Directly. This can be tricky sometimes, especially when dealing with nested data like in your example (a number as a property of an object inside an array).
Below, I refactored your code and added comments to help explain. By creating a function to update the state, and passing that function to each child component's props, the child component will be able to update the state by calling the function.
const {useState} = React;
// component dedicated to displaying each item in the array
const Item = (props) => (
<div style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<div>Amount: {props.item.amount}</div>
<div>User: {props.item.user}</div>
<button onClick={() => {
props.updateAmount(props.item.user);
}}>Increment</button>
</div>
);
const ItemList = () => {
const [data, setData] = useState([
{
id: '0',
user: 'Lisa',
amount: 1,
},
{
id: '1',
user: 'Walter',
amount: 3,
},
]);
const updateAmount = (user, changeAmount = 1) => {
// find the index of the item we want
const index = data.findIndex(item => item.user === user);
// return early (do nothing) if it doesn't exist
if (index === -1) return;
const item = data[index];
// don't modify state directly (item is still the same object in the state array)
const updatedItem = {
...item,
amount: item.amount + changeAmount,
};
// again, don't modify state directly: create new array
const updatedArray = [...data];
// insert updated item at the appropriate index
updatedArray[index] = updatedItem;
setData(updatedArray);
};
return (
<ul>
{data.map(item => (
<li key={item.id}>
<Item item={item} updateAmount={updateAmount} />
</li>
))}
</ul>
);
};
ReactDOM.render(<ItemList />, document.getElementById('root'));
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>
can you try like this once, pass second param as user id you want to update
<Button title="update" onPress={()=>setData(countAddFunc(data, 0))}></Button>
let countAddFunc =(getArr, id)=>{
const arrCopy = [...getArr]
const user = arrCopy.find(u => u.id === id )
if (user) {
user.amount++
}
return arrCopy
}
actually you are modifying the state directly, and we can not update state directly and getArr is just a refrence to the data in state, so, we created a copy of array, and modified copied array, and then we set this new array into the state, about the code throwing undefined error, add a check, if (user) user.amount++ and make sure id you send onPress={()=>setData(countAddFunc(data, 0))} actually exist
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Read the official documentation for more info here
Also, alternatively you can use useRef or useEffect with dependency array if you want the callback to immediately fire and do the changes.
I have a React application that is displaying a number of buttons in a modal that form part of a button group.
The values for these buttons are set up in the state object as shown below.
At the moment these buttons display within my modal each on a separate line one after the other.
I would like to have them displayed in a grid type structure. As there are 5 of them this might be 2 on the first 2 lines and then the last one
on the third line perhaps centered if possible.
How could I go about doing this? Sorry I realise I've not offered a solution attempt for this but I'm not sure where to start. I've been searching online
and can't find any examples that match what I am trying to do i.e. where the actual data for the table is set in state. I will continue to do more research but thought I'd ask on here to see if someone could offer any tips.
const marginSelectionControls = [
{ label: '21+', type: '21plus' },
{ label: '16-20', type: '16to20' },
{ label: '11-15', type: '11to15' },
{ label: '6-10', type: '6to10' },
{ label: '1-5', type: '1to5' }
];
const MarginSelectionControls = (props ) => (
<div className="btn-group">
<ButtonGroup toggle={this.toggle}>
{marginSelectionControls.map( ctrl => (
<MarginSelectionControl
key={ctrl.label}
label={ctrl.label}
selectMargin={(winningMargin) => props.selectMargin(ctrl.label)}
selectedmargin={props.selectedmargin}
/>
))}
</ButtonGroup>
</div>
);
##########################################
const MarginSelectionControl = (props) => {
const MarginSelectionControlClasses = [classes.PredResSelectControl];
if (props.selectedmargin === props.label) {
MarginSelectionControlClasses.push(classes.Less);
}
else {
MarginSelectionControlClasses.push(classes.More);
}
return (
<div className={classes.MarginSelectionControl}>
<button
type="button"
label={props.label}
className={MarginSelectionControlClasses.join(' ')}
onClick={props.selectMargin}
selectedmargin={props.selectedmargin}
>{props.label}</button>
</div>
)
};