React - UseEffect not re-rendering with new data? - reactjs

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>
);
}

Related

get current useState values inside functions (w/ short, explicit codepen)

I understand (somewhat) how to use useEffect to update state, but I struggle with situations like when you need current state inside of another function, before the "nextTick" as it were.
Here is a simple Codepen with the exact issue. Make sure the Pen console is open.
https://codepen.io/kirkbross/pen/vYRNpqG?editors=1111
const App = () => {
const [state, setState] = React.useState(null);
// how can I make sure the below function knows what the current state really is?
const handleAppend = (state) => {
console.log("click");
console.log(state?.text + " foobar");
};
return (
<div class="app">
<div className="row">
<span>Text: </span>
<input
type="text"
onChange={() => setState({ text: e.target.value })}
/>
</div>
<div className="row">
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
</div>
);
};
You're shadowing your state variable in your handleAppend function. You don't need to pass in an argument since state is available in scope of the component
const handleAppend = () => {
console.log("click");
console.log(state?.text + " foobar");
};
I did some changes. You dont need to use ur state as a parameter, since your textState lives inside your app component and there for you can reach it within your function.
Also, i changed the state and setState to textState, setTextState to make it less confusing. Also after clicking on the button and console logging, i cleared the textState so the next value wont be effected. Check it out below.
function App() {
const [textState, setTextState] = React.useState(null);
const handleAppend = () => {
console.log("click");
console.log(textState + " foobar");
setTextState('')
//also, you could make the input box clear after each press on button by adding value={textState} in the input.
};
return (
<div className="App">
<input
type="text"
onChange={(e) => setTextState(e.target.value)}
/>
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
);
}
My real world case was more complicated than the Pen. The actual function needing state was a useCallback function and I had forgotten to add state to the dep array of the useCallback function.
const handleDragEnd = useCallback(
async (result) => {
const { source, destination, draggableId } = result;
console.log(state); // shows up now.
},
[state], // I had forgotten to add state to the useCallback dep array
);

Dynamically create React.Dispatch instances in FunctionComponents

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>
))}
</>
);
}

Using React hooks, how can I update an object that is being passed to a child via props?

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

React not rendering new state

I am trying to learn the new React hooks. I have written a simple todolist that users can type in input to create a new todo, and click on the todo to remove it. However, it is not re-rendering after an todo item is removed.
Here is my code
import React, { useState, Fragment } from "react";
const Todo = () => {
const [inputVal, setInput] = useState("");
const [list, setList] = useState([]);
const handleInput = e => {
setInput(e.target.value);
};
const handleClick = () => {
setList([...list, inputVal]);
setInput("");
};
const handleDelete = index => {
setList([...list.splice(index, 1)]);
console.log(list);
};
const renderList = () =>
list.map((item, index) => {
return (
<div key={index} onClick={() => handleDelete(index)}>
{item}
</div>
);
});
return (
<Fragment>
<div>
<input value={inputVal} onChange={handleInput} />
<button onClick={handleClick}>submit</button>
</div>
<ul>{renderList()}</ul>
</Fragment>
);
};
export default Todo;
You have two issues here.
array.splice is mutating and returns the removed items.
When you run
setList([...list.splice(index, 1)]);
This removes one item from the object list and then calls setList([removed_item]).
You could replace this line with
setList(list.slice(0, index).concat(list.slice(index + 1))
What is currently happening is that you are setting the state as the same object as before (but mutated), so it doesn't trigger a re-render.
You are not using key attributes correctly. You can read the docs here
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.
Your keys should uniquely identify the elements of the todo list, without reference to the list itself. The text in the item is a good choice, except for the possible problem of non-uniqueness.
When you re-render the todo list, React uses the keys to decide which children to re-render. If you use indexes as keys, and then remove the item at index 0, then the 0th child will not be told to update to the new 0th element because it still gets the same key.

Re-rendering only updated elements on react form

I want to achieve a dynamically built form with controlled components that scale well. The problem I'm having is updating the state is causing the entire form to re-render (or something expensive?) causing epic type lag.
// Generate some example fields
let defaultFields = {};
for(let n=0; n<1000; n++){
defaultFields[n.toString()] = {'id':n.toString(), 'value':'','label':'Field '+n}
}
const [valuesById, setValuesById] = useState(defaultFields);
const updateValueCallback = React.useCallback((e)=>{
e.preventDefault();
e.persist();
setValuesById(prevValuesById => {
let fieldId = e.target.id;
return {...prevValuesById,
[fieldId]:{
'id':fieldId,
'value':e.target.value,
'label':'Field '+fieldId
}};
});
});
return <div>
{ Object.entries(valuesById).map(([id,formField]) => {
return <p key={formField.id}>
<label>{formField.label}
<SingleLineStringInput isRequired={false} value={formField.value} onChangeCallback={updateValueCallback} id={formField.id} name={'name_'+formField.id} />
</label>
</p>
})
}
</div>;
If the props aren't changing for 999 of the fields then why do they re-render? Or what is actually happening here (the fields don't actually flash in the debug tool but the parent does)? I really need help to understand this better and a fix which isn't too drastically different from what I've done as we've built a large amount of logic on top of this basic structure and have only now realised that it's not scaling.
SingleLineInput:
const SingleLineStringInput = React.memo(({name, id, value, onChangeCallback}) => {
console.log("UPDATING "+id);
return <input className={'input ' + inputClasses.join(' ')} name={name} id={id} type="text"
value={(value === null) ? '' : value}
onChange={onChangeCallback} />
});
Ok I will try to help, you are doing fine using memo and useCallback, but you are not passing the array of dependencies to useCallback, as your callback would be same for every render, you can pass an empty array to it, so it will be the same function on every render,
You can do it as this
const updateValueCallback = React.useCallback((e)=>{
e.preventDefault();
e.persist();
setValuesById(prevValuesById => {
let fieldId = e.target.id;
return {...prevValuesById,
[fieldId]:{
'id':fieldId,
'value':e.target.value,
'label':'Field '+fieldId
}};
});
}, []); // dependencies array
Now if you change a field value on that one will be re-rendered
Sandbox to show you how its done
Hope it helps

Resources