I'm trying to have a function run everytime I click a checkbox that adds the id for that it to an array so that I can then use that array to make an apollo call to a mutation in graphQL. What is currently happening is that every checkbox gets checked when I press any of them and arrays are consoled for every ID in the table. What am I doing wrong here?
const RouteLocationsSelector = (props) => {
const {count} = useMileState()
const dispatch = useMileDispatch()
const [checked, setChecked] = useState(false);
const handleClick = () => setChecked(!checked)
var locationArray = [];
function checkedLocations(id) {
if (!locationArray.includes(id)) {
return locationArray.push(id)
} else {
return locationArray.filter(function(e) { return e != id})
}
}
const { loading, error, data } = useQuery(GET_LOCATIONS);
if (loading) return <tbody><tr><td>Loading...</td><td></td><td></td></tr></tbody>;
if (error) return <tbody><tr><td>Errror, are you logged in?</td><td></td><td></td></tr></tbody>;
return data.locations.map(({ id, slug, gps }) => (
<tbody key={id}>
<tr>
<td>{id}</td>
<td>{slug}</td>
<td>{gps}</td>
<td>
<input type="checkbox"
onClick={handleClick}
onChange={checkedLocations(id), console.log(locationArray)}/>
</td>
</tr>
</tbody>
))
};
export default RouteLocationsSelector;
You should probably make the input box a fully controlled component by adding the checked property to it and setting it equal to the value of your local component state.
But also, since the check boxes are in a mapped array, you'll want to handle the state of whether or not they are in a checked state separately. So each checkbox should have it's own state stored in an array. So I imagine your useState hook should be an array of false values that get swapped to true for the index position of the checkbox that has changed.
Might also be a question of composition as well. You could abstract what is returned by the map function into it's own component so it can handle its own local state.
<input type="checkbox"
checked={checked}
onClick={handleClick}
onChange={() => { checkedLocations(id); console.log(locationArray)}}
/>
I also imagine you'd probably want to add an anonymous function to the onChange event so that it doesn't fire everytime.
Related
I'm trying out React and trying to make a simple component. Input and "Add" button.
I want get a list of values after filling in the input and clicking on the button. I can see that the state is getting filled, but I don't understand why the list is not being rerender.
Here is my code https://jsfiddle.net/3hkm2qnL/14/
`
const InputWithAddBtn = props => {
const [ value, setValue ] = React.useState('');
return (
<div>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
<button onClick={() => props.add(value)}>+</button>
</div>
);
};
`
The problem is in the add() function, which by pushing onto the original array does not signal to the component to rerender.
const add = (value) => {
initValue.push(value)
console.log(initValue)
setValue(initValue)
}
One possible solution:
const add = (value) => {
const newValues = [...initValue, value]
console.log(newValues)
setValue(newValues)
}
That will trigger correctly the component to rerender.
For more info see https://stackoverflow.com/a/67354136/21048989
Cheers
How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?
if I have the following (untested, simplified example):
interface Foo {
val: string;
setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
const [val, setVal] = useState('')
setAllVals(allVals.concat({val, setVal}))
}
return (
<input type="button" value="Add input" onClick={addVal}>
allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
)
}
I will get the error Hooks can only be called inside of the body of a function component.
How might I dynamically add "watched" elements in the above code, using FunctionComponents?
Edit
I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.
Edit 2
in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:
An add button
Each time the button is pressed, another input element is added.
Each time a new value is entered into the input element, the input element shows the value
There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.
Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
setAllVals(allVals => {
// `i` would be the fixed index of newly added item
// it's captured in closure and would never change
const i = allVals.length
const setVal = (v) => setAllVals(allVals => {
const head = allVals.slice(0, i)
const target = allVals[i]
const tail = allVals.slice(i+1)
const nextTarget = { ...target, val: v }
return head.concat(nextTarget).concat(tail)
})
return allVals.concat({
val: '',
setVal,
})
})
}
return (
<input type="button" value="Add input" onClick={addVal} />
{allVals.map(v =>
<li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
)}
)
}
React hooks cannot be called in callbacks as this breaks the Rules of Hooks.
From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.
interface Foo {
val: string;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([]);
const addVal = () => {
setAllVals((allVals) => allVals.concat({ val: "" }));
};
const updateVal = (index: number) => (e: any) => {
setAllVals((allVals) =>
allVals.map((el, i) =>
i === index
? {
...el,
val: e.target.value
}
: el
)
);
};
return (
<>
<input type="button" value="Add input" onClick={addVal} />
{allVals.map((v, i) => (
<li key={i}>
<input value={v.val} onChange={updateVal(i)} />
</li>
))}
</>
);
}
I am getting a book list from database and is stored in a state variable
Book list also has book price field
const [books, setBooks]=useState([])
setBooks(data)
I am creating an html in a loop based on this data
return ( <div>
{books.map((x,i) => ( <tr>
<td>x.bookName</td>
<td><MyCustomTextInput value={x.price}></MyCustomTextInput></td>
<tr></div>);
Following is the implementation of MyCustomTextInput
function MyCustomTextInput(props)
{ return (<div>><MyCustomTextInput></MyCustomTextInput> </div>)
} exports default MyCustomTextInput
My question is: when I change a price in the custom text input appeared in the table, it does not change in the underlying state level variable's array element
Could you please suggest?
to change a price in books state update books state like that
const handleChange = (e, id) => {
setBooks(books => books.map(book => {
if (book.id === id) book.price = e.target.value;
return book;
}))
};
//you must pass onChange prop to Input
<MyCustomTextInput onChange={e => handleChange(e, x.id)} value={x.price}>
//pass onChange prop to Input like this
function MyCustomTextInput(props){
return <TextInput onChange={props.onChange}/>
}
This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}
I am building a table component. It gets as a prop an object called content which holds records that are displayed as the table's content. The component has a state called 'currentRecord' which holds the id of the selected row (changes in onClick event on each row).
I want to set the first record's id to be the initial state using the 'useState'.
As an initial state argument for the 'useState' it has a function which return the key(which is the id) of the first record in the content prop. But it returns undefined. When console logging the return value of that function, it return an id.
Why does it return undefined when setting the initial state using a function?
I have tried setting the initial state using a string instead of a function, and it worked.
function getFirstOrderId(content:object): string {
return Object.keys(content)[0];
}
const Table: FunctionComponent<Props> = props => {
const { columnTitles, content, onRowClick } = props;
const [currentRecord, setCurrentRecord] = useState(getFirstOrderId(content));
useEffect(() => {
onRowClick(currentRecord);
}, [currentRecord]);
return (
<StyledTable>
<thead>
<tr>
{Object.values(columnTitles).map(fieldName => {
return <th>{fieldName}</th>;
})}
</tr>
</thead>
<StyledTBody>
{mapWithKeys((order: any, id: string) => {
return (
<StyledRow
key={id}
isSelected={id === currentRecord}
onClick={() => setCurrentRecord(id)}
onDoubleClick={() => window.open("/" + order)}
>
{Object.keys(columnTitles).map(fieldContent => {
return <td>{order[fieldContent]}</td>;
})}
</StyledRow>
);
}, content)}
</StyledTBody>
</StyledTable>
);
};
export default Table;
Put a function inside the useState hook and return the value.
const [value, setValue] = useState(() => ({key: "Param"}));
console.log(value) // output >> {key: "Param"}
This might work:
const [currentRecord, setCurrentRecord] = useState(null);
useEffect(()=>{ // This will run after 1st render
setCurrentRecord(getFirstOrderId(content)); // OPTION 1
setCurrentRecord(()=>{ // OPTION 2
return getFirstOrderId(content);
});
},[]);
You can set up a loading state to wait for the useEffect() to take place.
You can actually do lazy initialisation to state with a function. how ever you called the function and not passed in as a parameter, meaning you passed the returned value of the function as the initial value to use state.
You can check out the official explanation:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all