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>
))}
</>
);
}
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
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>
);
}
You can check the full code here:
CODESANDBOX LINK
This is really weird.
I have a list of input fields which are rendered dynamically depending on the button click
const newFields = (index) =>
<div>
<input value={inputs[index].name} onChange={e => handleNameInput(e,index)} />
<input value={inputs[index].age} type="number" onChange={e => handleAgeInput(e,index)} />
<Button onClick={() => setInputArray(inputArray => [...inputArray, newFields(++index)])}>Add</Button>
</div>
I'm trying to store the values of the inputs and update them onChange. But as stated in the title, I can't type in the input field and once I click a character the input loses focus. I don't know why is that happening.
Here are the states and the handleInput function:
let [inputs, setInputs] = useState(Array.apply(null, Array(100)).map(function () { return {
name: '',
age: ''
} }));
const handleNameInput = (e, i) =>{
let tempArr = new Array();
tempArr = [...inputs];
tempArr[i].name = e.target.value;
setInputs([...tempArr]);
}
//handleAgeInput is exactly the same
I think your main issue is the duplicate usage of arrays. The facts is, you want only one array of datas. So, you need to build your component around it. If you make another array on state, you're adding complexity and it's resulting to you're current issue.
Understand why there is no updates
You're onChange function call update the inputs array state. BUT, you're rendering the inputArray where nothing change to him. No setInputArray call so, no re-render.
Proposal
There is proposal implementation of what I understand of you needs :
function App() {
let [inputArray, setInputArray] = useState([{
name: "",
age: ""
}]);
const handleNameInput = useCallback(
(e, i) => {
inputArray[i].name = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleAgeInput = useCallback(
(e, i) => {
inputArray[i].age = e.target.value;
setInputArray([...inputArray]);
},
[inputArray]
);
const handleNewField = useCallback(() =>
setInputArray(inputArray => [...inputArray, {
name: "",
age: ""
}]),
[]
);
return (
<div className="App">
{inputArray.map((element, i) => (
<div>
<input
value={element.name}
onChange={e => handleNameInput(e, i)}
/>
<input
value={element.age}
type="number"
onChange={e => handleAgeInput(e, i)}
/>
<button
onClick={handleNewField}>
Add
</button>
</div>
))}
</div>
);
}
Depending of you needs maybe you should adapt it. There is some ways to avoid re-render the whole list. But this way you can edit all fields
One another proper way to do this is to to make each fields group as another Component.with independents states.
If you want the button juste once, move him from the loop.
If you want to display only the last fields set, take the inputArrays[inputArray.length] instead of loop on all.
Explanation :
You're rendering an array of components { inputArray.map((e, i) => { return <div key={i}> {e} </div>; })} where e should be a component.
In another hand, you're maintaining an array of datas that's "shadowing" the first array.
The main concept of your use case as far I can understand, should be to render your components directly from your datas – your array of datas.
This way you have only one source of truth.
I wish to generate inputs based on json, so first I set it to initial state, then in child componenet I want to modify it's field, thing is that component doesnt update... It renders once and have no idea how to make it be updated each time when input onChange change it's value. Any idea how to make value of input be updated each time when I type something?
PARENT
function App() {
const [inputValue, setInputValue] = useState(chunkingRow);
const handleChunkingChange = (e, index) => {
let inputContent = inputValue;
const reg = new RegExp(/^[0-9]+$/);
if (e.target.value.match(reg)) {
inputContent[index] = {
...inputContent[index],
content: e.target.value,
};
setInputValue(inputContent);
console.log(inputValue);
} else console.log('not a number')
};
return (
<div>
<Wrapper>
{Chunk(inputValue, handleChunkingChange)}
</Wrapper>
</div>
);
}
CHILD
const Chunk = (inputValue, handleChunkingChange) => {
return(
<div>
{inputValue.map((el, index) => (
<div key={index}>
<p>{el.title}</p>
{console.log(el.content)}
<input
type="text"
onChange={(e, i) => handleChunkingChange(e, index)}
value={el.content}
/>
</div>
))}
</div>
);
}
link to demo
https://codesandbox.io/s/stoic-mirzakhani-46exz?file=/src/App.js
Not completely sure why this happens, but probably because of the way you handle the input change. It seems to me that component doesn't recognize that array changed. How I managed to fix your code is replacing line 9 in App component with following code:
let inputContent = [...inputValue];
By doing that, array's reference is changed and components are updated.
Just update your code as follow:
let inputContent = [ ...inputValue ];
You are mutating the state object.
let inputContent = inputValue;
That's why the state is not re-rendered. Change it to
let inputContent = [...inputValue];
An example of mutating objects. React compares previous state and current state and renders only if they are different.
const source = { a: 1, b: 2 };
const target = source;
console.log(target);
console.log(target === source); = true
target.b = 99;
console.log({target});
console.log({source}); //source == target due to mutation
console.log(source === target); = true
Remember, never mutate.
Currently learning React and need to get a prop of a clicked element. I managed to get it, but it does not feel like the "right" react way.
This is what I have (it works):
filterProjects = (event) => {
const value = event.currentTarget.getAttribute("filterTarget")
console.log(value)
}
Initially I tried multiple things, e.g.:
const value = this.props.filterTarget or const value = event.currentTarget.this.props.filterTarget or even using ref but all returned undefined when console logging the value.
(Using this.props as it's part of a class Component.)
This is what my target element looks like:
const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
return (
<a
key={index}
onClick={this.filterProjects}
filterTarget={cat.node.uid}
>
{cat.node.data.category.text}
</a>
)
})
One simple way is passing the value itself,
onClick={() => this.filterProjects(cat.node.uid)}
And the function,
filterProjects = (value) => {
console.log(value)
}