I have this renderUpgrades-function in which the options of an item get included into radio-button-groups. So an item has multiple options where each option has a a radio-button-group. I know that a radio-button-group can be handled with useState where each useState gets a group assigned. But in my case I don't know how many options an item has so I can't initialize the exact amount of useStates at the beginning. Is there a way how I can initalize the useStates depending on how many options there are or is there another way how the radio-button-goups can be handled?
const renderUpgrades=(item)=>{
return item.optionModules.map((optionModule,index)=> {
console.log(optionModule.module)
if (optionModule.module && optionModule.module.selectionRequired) {
return(
<div key={index}>
<h4>{optionModule.module.name}</h4>
{optionModule.module.options.map((moduleOptions) => {
if(optionModule){
return (
<div onChange={()=>{}}>
<label><input type="radio" value={moduleOptions.option.name} name={index} checked={moduleOptions.isDefault}/> {moduleOptions.option.name}</label>
</div>
)
}else{
return console.log("No shifts applied");
}
})
}
</div>
)
}})
}
You can use an object as state.
const [radioGroups, setRadioGroups] = useState({});
The initialization can be done separately, for example in a useEffect with empty dependency array:
useEffect(() => {
const groups = {};
// Loop through your radio groups here, I don't think I got the right array
item.optionModules.forEach(module => {
groups[module.option.name] = "default selected value";
});
setRadioGroups(groups);
}, []);
Then everytime you have to edit a group you get the current state and edit the group
setRadioGroups({ ...radioGroups, [groupToBeChanged]: groupValue });
Related
I have created a basic Todo app but having a hard time in implementing proper focus into it. Following are the requirements for focus:
Focus should be on the newly created input field when "Add" is clicked so the user can begin typing right away.
On deleting an item "Button X", focus will move to the input field in the row which replaced the deleted row, nowhere if there are no fields left, or on the new last row if the last row was deleted.
On moving an item up, focus should be placed on the newly moved field, and all associated buttons should move alongside the element. If a field is already at the top of a list, no reordering should occur, but focus should be transferred to the topmost field nonetheless.
On moving an item down, same principle should apply here (focus should be placed on the field that is moved down). If its the last field, focus it nonetheless.
Here is my implementation.
App.js:
import React, { useState, useRef } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
focusInput.current[myRows.length - 1].focus();
};
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
In my implementation when I click Add, focus changes but in unexpected way (First click doesn't do anything and subsequent click moves the focus but with the lag, i.e. focus should be on the next item, but it is at the prior item.
Would really appreciate if someone can assist me in this. Thanks!
The strange behaviour that you observe is caused by the fact that state updates are asynchronous. When you, in onAddRow, do this:
focusInput.current[myRows.length - 1].focus()
then myRows.length - 1 is still the last index of the previous set of rows, corresponding to the penultimate row of what you're actually seeing. This explains exactly the behaviour you're describing - the new focus is always "one behind" where it should be, if all you're doing is adding rows.
Given that description, you might think you could fix this by just replacing myRows.length - 1 with myRows.length in the above statement. But it isn't so simple. Doing this will work even less well, because at the point this code runs, right when the Add button is clicked, focusInput hasn't yet been adjusted to the new length, and nor in fact has the new row even been rendered in the DOM yet. That all happens a little bit later (although appears instantaneous to the human eye), after React has realised there has been a state change and done its thing.
Given that you are manipulating the focus in a number of different ways as described in your requirements, I believe the easiest way to fix this is to make the index you want to focus its own piece of state. That makes it quite easy to manage focus in any way you want, just by calling the appropriate state-updating function.
This is implemented in the code below, which I got working by testing it out on your Codepen link. I've tried to make it a snippet here on Stack Overflow, but for some reason couldn't get it to run without errors, despite including React and enabling Babel to transform the JSX - but if you paste the below into the JS of your Codepen, I think you'll find it working to your satisfaction. (Or, if I've misinterpreted some requirements, hopefully it gets you at least a lot closer than you were.)
Rather than just leaving you to study the code yourself though, I'll explain the key parts, which are:
the introduction of that new state variable I just mentioned, which I've called focusIndex
as mentioned, the calling of setFocusIndex with an appropriate value whenever rows are added, removed or moved. (I've been trying to follow your requirements here and it seems to work well to me, but as I said, I may have misunderstood.)
the key is the useEffect which runs whenever focusIndex updates, and does the actual focusing in the DOM. Without this, of course, the focus will never be updated on calling setFocusIndex, but with it, calling that function will "always" have the desired effect.
one last subtlety is that the "always" I put above is not strictly true. The useEffect only runs when focusIndex actually changes, but when moving rows there are some situations where it is set to the same value it had before, but where you still want to move focus. I found this happening when clicking outside the inputs, then moving the first field up or the last one down - nothing happened, when we want the first/last input to be focused. This was happening because focusIndex was being set to the value it already had, so the useEffect didn't run, but we still wanted it to in order to set the focus. The solution I came up with was to add an onBlur handler to each input to ensure that the focus index is set to some "impossible" value (I chose -1, but something like null or undefined would have worked fine as well) when focus is lost - this may seem artificial but actually better represents the fact that when the focus is on no inputs, you don't want to have a "sensible" focusIndex, otherwise the React state is saying one of the inputs is focused, when none are. Note that I also used -1 for the initial state, for much the same reason - if it starts at 0 then adding the first row doesn't cause focus to change.
I hope this helps and my explanations are clear enough - if you're confused by anything, or notice anything going wrong with this implementation (I confess I have not exactly tested it to destruction), please let me know!
import React, { useState, useRef, useEffect } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const [focusIndex, setFocusIndex] = useState(-1);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
setFocusIndex(myRows.length);
};
useEffect(() => {
console.log(`focusing index ${focusIndex} in`, focusInput.current);
focusInput.current[focusIndex]?.focus();
}, [focusIndex]);
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
setFocusIndex(index - 1);
} else {
setFocusIndex(0);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
setFocusIndex(index + 1);
} else {
setFocusIndex(myRows.length - 1);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
const newFocusIndex = index < currentState.length
? index
: currentState.length - 1;
setFocusIndex(newFocusIndex);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
onBlur={() => setFocusIndex(-1)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
I tried some solutions on the net but cant fix it so i need to ask. I have checkboxes which is mapped in render. I want to add the values of the checkboxes to array state if checked or remove the value if unchecked. Thanks for helps Things i tried ;
My jsx ;
{props.edit && <input type="checkbox" value={props.id} onChange={(e) => setDeleteId(e)}/>}
Functions ;
const [deleteId, setdeleteId] = useState([]);
const setDeleteId = (e) => {
setdeleteId([...deleteId, e.target.value]);
}
useEffect(() => {
console.log(deleteId);
}, [deleteId])
Your event handler looks like it's always just adding the checkbox id to the state - you need to also remove it when the checkbox gets unchecked, e.g. -
const [selected, setSelected] = useState([]);
const handleChange = event => {
const { checked, value } = event.currentTarget;
setSelected(
prev => checked
? [...prev, value]
: prev.filter(val => val !== value)
);
};
Here is a demo on StackBlitz showing how it all fits together.
I am retrieving a list of data and search for a title name from a list of todos, then i have built with Switch Material UI a component to toggle the todos that are completed and not.
You can see the full demo here => https://codesandbox.io/s/so-68247206-forked-sx9kv?file=/src/App.js
Now i want to search the todos title in the list of the todos/tasks that are completed or not completed, depending on where you switch, at the moment the search goes through in all the todos.
To reproduce the case
Launch the app in codesandbox
Switch the toggle
Search for elements (It search in all the todos list and not through the completed or not completed result list )
The relevant code is the following
The search method
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
result = tasks.filter((data) => {
return data.title.search(value) !== -1;
});
setFilteredData(result);
};
The complete method
const getCompleted = (e, value) => {
let result = [];
let switchValue = e.target.checked;
result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(!switchValue);
setText(isCompleted ? "Yes" : "No");
};
How can i change my search method to look through the todos completed and not completed?
There's a slight modification to your handleSearch method. When you're filtering your tasks, you can check if you have toggled on the completed switch using the isCompleted state. All you need is an additional condition inside your filter method that takes care of the is completed scenario. The following updated handleSearch method works fine.
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = [];
console.log(value);
let filterCompleted = false;
if (!isCompleted) filterCompleted = true;
console.log({ filterCompleted });
result = tasks.filter((data) => {
if (filterCompleted)
return data.completed && data.title.search(value) !== -1;
else return !data.completed && data.title.search(value) !== -1;
});
setFilteredData(result);
};
I use a local variable inside handleSearch to check if you have toggled on the completed button. If it's toggled on, while filtering I only filter those tasks that have the completed property set to true and vice versa. You can check that this works here https://codesandbox.io/s/so-68247206-forked-4iz7p?file=/src/App.js.
Note: I feel the isCompleted state is working on a reverse logic. When I toggle on the switch, the isCompleted is false but it should be true instead. You can try fixing this or renaming the state (if the reverse logic was intentional and you only messed up with naming the state). Anyhow this doesn't affect the solution but makes it difficult to debug and adds some confusion.
Your filter in handleSearch searches from the original tasks array which might not be what you want :
const handleSearch = (event) => {
let value = event.target.value.toLowerCase();
let result = tasks; //Assigning initial value to result
if(isCompleted){
result = result.filter(({ completed }) => completed === isCompleted)
} //only if isCompleted is true, filter the array first
result = result.filter((data) => {
return data.title.search(value) !== -1;
}); //Filter the array based on search term
setFilteredData(result);
};
Also, your getCompleted is setting the inverse value of the checkbox with (!switchValue). You can change that along with the extra param value which is not accounted for in your onChange call.
const getCompleted = (e) => {
let switchValue = e.target.checked;
let result = tasks.filter(({ completed }) => completed === switchValue);
setFilteredData(result);
setIsCompleted(switchValue);
setText(switchValue ? "Yes" : "No"); //Is text state variable really required?
};
Here I have tried to fix it : CodeSandbox
That being said, I would recommend not having multiple state variables which can be derived from each other. Your text state variable is only used for view purposes and its value completely relies on isCompleted. You could have used a conditional operator in your return function to show "Yes/No" instead of a new state variable.
You have to save both the search value and switch value in state and use a useEffect to filter the values. Check the below fork of your code
https://codesandbox.io/s/so-68247206-forked-nkuf3?file=/src/App.js
I am experiencing a very odd issue with my react code : useState isn't updating the view and after literally trying everything the issue is still there. I made a simple code to explain the issue :
function(){
const [enterJob, setEnterJob] = useState(false);
const [jobSelection, setJobSelection] = useState(Array(someList.length).fill(false));
const jobRef = useRef();
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = jobSelection;
c[n] = !c[n];
setJobSelection(c);
};
const handleMouse = (e) =>{
if (!jobRef.current.contains(e.target)){
setEnterJob(false);
};
};
useEffect(() => {
window.addEventListener("mousedown", handleMouse);
return () => window.removeEventListener("mousedown", handleMouse);
});
return(
<div ref={jobRef}>
<input onFocus={()=> setEnterJob(true)} />
<div style={{display: `${enterJob ? 'flex' : 'none'}`}} >
<ul>
{ someList.map((item,index)=>
<li id={`${index}`} onClick={handleJobClick}> {jobSelection[index] ? item : "you clicked on the button"} </li> )}
</ul>
</div>
</div>
)
}
Some explanations: I am using UseEffect and useRef to create a dropDown menu that disappears when you clic outside the container. Now when I want to clic on a value of this drop-down menu it doesn't update the DOM while I am using useState to update the value of the string responsible for the change.
Thank you in advance,
Charbel
The problem is that you are mutatiing your jobSelection instead of creating a new object. And react will skip the rerender if the the objects has the same reference as before:
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = [...jobSelection]; // Create a new array
c[n] = !c[n];
setJobSelection(c);
};
Issues
If I understand your issue then I believe it is because you are directly mutating your state.
const handleJobClick = i => {
const n = parseInt(i.target.id.charAt(0)); // the list is small enough to allow this
let c = jobSelection;
c[n] = !c[n]; // <-- mutation!
setJobSelection(c);
};
You are also missing react keys on the mapped list items.
Solution
Since the next state depends on the previous state you should use a functional state update to copy your state first, then update it.
I suggest:
converting handleJobClick to consume the index directly, a curried function handles this cleanly
Add a react key to the mapped list items
Code
const handleJobClick = index => () => {
setJobSelection(jobSelection => jobSelection.map(
(selection, i) => index === i ? !selection : selection // <-- toggle selection at matched index
);
};
...
<ul>
{someList.map((item, index)=> (
<li
key={index} // <-- index as react key, ok since not adding/removing/sorting jobs
onClick={handleJobClick(index)} // <-- pass index to handler
>
{jobSelection[index] ? item : "you clicked on the button"}
</li>
))}
</ul>
I'm making a little game where the user guesses which of the two foods has the most calories. I scraped data on 400+ food items and imported in my component.
I want to make sure that both randomly selected food items do not have the same number of calories. I put a while loop in componentDidMount to check if it's true. If true, it will setState to another randomly selected food – repeat until they're no longer the same number.
For now, I have my while loop's condition set to '!==' for testing. This will be changed to '===' after I know it works the other way. The problem I'm having is that in my console, the foods will always be the same objects because setState isn't working for some reason.
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
}
Another issue I'm facing is that in my debugger, when I try to manually enter Foods[Math.floor(Math.random() * Foods.length)], I get a "Food is not defined" although it works in the state initialization. However, when I enter _foods_json__WEBPACK_IMPORTED_MODULE_1__ in console from autofill, I have all of my data there so this works (replaced the word "Foods"):
_foods_json__WEBPACK_IMPORTED_MODULE_1__[Math.floor(Math.random() * _foods_json__WEBPACK_IMPORTED_MODULE_1__.length)]
I can't use that line of code in React, though.
http://www.giphy.com/gifs/UqwKkdF2ok2hlTwlLB
I would suggest slightly different approach for generating a pair of pseudo-random items, that may not save you CPU cycles, but will certainly avoid loops that led you to your current position.
So, the idea is rather simple:
you pick first random item
then you filter out the items with the same nutritional value
you pick another random item from remaining ones
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
You may find the demo of that concept below:
//dependencies
const { useState, useEffect } = React,
{ render } = ReactDOM
//sample data set
const data = [{name:'BBQ Ranch Burger',value:350},{name:'Big Mac',value:540},{name:'Double Cheesburger',value:440},{name:'Bacon Cheddar McChicken',value:540},{name:'Chicken McNuggets (10 piece)',value:440},{name:'Bacon Biscuit',value:350}]
//food card component
const FoodCard = ({name,value}) => (
<div style={{width:100,height:150,display:'table-cell', border:'1px solid black',verticalAlign:'middle',textAlign:'center'}}>{name}<br/>(calories: {value})</div>
)
//game board component
const GameBoard = () => {
//store foods within local state
const [foods,setFoods] = useState([]),
//describe thw way of picking two pseudo-random items
pickTwo = arr => {
const firstItem = arr[0|Math.random()*arr.length],
filteredArr = arr.filter(({value}) => firstItem.value != value),
secondItem = filteredArr[0|Math.random()*filteredArr.length]
return [firstItem, secondItem]
}
//pick two cards on initial render
useEffect(() => setFoods(pickTwo(data)),[])
//return component
return (
<div>
<FoodCard {...foods[0]} />
<FoodCard {...foods[1]} />
<button onClick={() => setFoods(pickTwo(data))}>reset</button>
</div>
)
}
render(
<GameBoard />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
The reason your loop will run endlessly is that, when you call setState the state is not actually immediately updated, there is a lag on it, which has to do with the React component lifecycle.
So instead of calling the setState in the loop. Declare a local variable, update the variable in the loop and when the loop is done, then set the state.
componentDidMount() {
let myFood = this.state.firstFood
while (
// to check that the two foods' calories are not the same
myFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
myFood = Foods[Math.floor(Math.random() * Foods.length)]
}
this.setState({
firstFood: myFood
});
}
I did not test this in detail but that should give you the idea.
State is not just some variable that you set whenever you want. It is reactive. It causes re-renders. You should pick two foods you want to have, make sure they don't have the same calories and only then do you setState them
There is no reason to setState on componentDidMount, your logic belongs in constructor
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
let secondFood
do {
secondFood = allFoods[Math.floor(Math.random() * allFoods.length)]
} while (firstFood.attributes.calories !== secondFood.attributes.calories)
return { firstFood, secondFood }
}
class Game extends Component {
constructor(props) {
super(props)
const { firstFood, secondFood } = _pickFoods(Foods)
this.state = {
firstFood,
secondFood
}
}
...
}
That is if you want this while logic in your foods. It will loop forever if your Foods array consists of unviable foods only. Try this better alternative
const _pickFoods = (allFoods) => {
const firstFood = allFoods[Math.floor(Math.random() * allFoods.length)]
const possibleSecondFoods = allFoods.filter(food => food !== firstFood && food.attributes.calories !== firstFood.attributes.calories)
const secondFood = possibleSecondFoods[Math.floor(Math.random() * possibleSecondFoods.length)]
return secondFood
}
Try this:
import Foods from "../foods.json";
class Game extends Component {
state = {
firstFood: Foods[Math.floor(Math.random() * Foods.length)],
secondFood: Foods[Math.floor(Math.random() * Foods.length)],
mostCalories: "placeholder"
};
componentDidMount() {
setTimeout( () => {
while (
// to check that the two foods' calories are not the same
this.state.firstFood.attributes.calories !==
this.state.secondFood.attributes.calories
) {
console.log(this.state.firstFood.attributes.calories);
console.log(this.state.secondFood.attributes.calories);
debugger;
// an infinite loop because the below setState function doesn't run properly
this.setState({
firstFood: Foods[Math.floor(Math.random() * Foods.length)]
});
}
},2000)
}
I think it is because the data is not ready yet.