How to render multiple objects based on a changing number - reactjs

I've done this in Vue before with v-for. But in React I want to add a TextField based on a number in my state. I have a state:
constructor(...props){
super(...props)
this.state = {
questions: 1
}
}
My component will have a button which will add 1 to questions if pressed. And I want to render TextFields based on the number in the state (questions). By that same logic I also plan on doing a remove button which will remove one from the total, and will also remove one of the TextFields. I tried this:
render(){
let category_rows = []
for(var i = 0; i < this.state.questions; i++){
category_rows.push(<TextField variant='outlined' />)
}
return(
<Card>
<CardContent>
<Typography>
Add Question Categories
</Typography>
{category_rows}
</CardContent>
<Button onClick={() => {
this.state.questions += 1
console.log(this.state)
}}>Add</Button>
</Card>
)
}
}
And it renders the first time but when pressing my button and adding to the state, it doesn't add a new field like I want.
While I'm at it, if someone could tell me if it's possible to take the code on the Button's onClick and put it in a function, then call the function on the onClick. I tried it but when I log this it says it is undefined.

you are changing the state variable wrong
inside your Button instead of doing this
<Button onClick={() => {
this.state.questions += 1
console.log(this.state)
}}>Add</Button>
do this
<Button onClick={() => this.setState({questions : this.state.questions +1 })}>Add</Button>
Because in react we shouldn't update the state variable directly. otherwise it will not trigger re render and wont update.

You are updating the state wrong way. State should always be immutable and should only be updated by using setState function
<Button onClick={() => {
this.setState({questions: this.state.questions += 1})
console.log(this.state)
}}>Add</Button>

Related

Changing the React component state with same value on button click trigger re-render for the first two button clicks. Why UI re-rendering twice?

When I click on the 'Rerender UI' button then it prints in the console for the first two clicks. I think it should print in the console only for the first time when I click on the 'Rerender UI' button because on the button click the component state is changed so UI will re-render and the console log will be printed in the console. Why is it printing for the second click? StrictMode is off. See code:
export default function UseCallbackComp() {
const [stateVar, setStateVar] = useState<any>()
console.log("Parent Rerendered!")
return (
<>
<div>UseCallbackComp content</div>
<div>
<button
onClick={() => {
setStateVar(1)
}}
>
Rerender UI
</button>
</div>
</>
)
}
When I put the console log line inside useEffect like below it prints only for the first time 'ReRender UI' button is clicked which is the expected behaviour.
useEffect(() => {
console.log("Parent Rerendered!")
})
From the below two links I got to know whats the behaviour of react when useState is used:
stackoverflow question
gitHub discussion
There are two different cases for which useState behaves differently for same value.
Case 1: When useState used with same value as the initial value.
Result: No rerendering at all.
export default function LifecycleEvents() {
const [stateVar, setStateVar] = useState<any>(0)
console.log("Parent Rerendered!")
return (
<>
<button
onClick={() => {
setStateVar(0) //same value as the initial value
}}
>
Rerender UI
</button>
</>
)
}
Case 2: When useState used with different value first and then with the same value.
Result: First both Parent and child will be re-rendered. But for the second time, only the render function of Parent component will get called, Nothing else.
See code:
export default function LifecycleEvents() {
const [stateVar, setStateVar] = useState<any>(0)
console.log("Parent Rerendered!")
return (
<>
<button
onClick={() => {
setStateVar(1) //different value then initial value.
}}
>
Rerender UI
</button>
</>
)
}
Conclusion: Nothing to worry unless you have an expensive render method of the component. In that case use memo.

setState second argument callback function alternative in state hooks

I made a code sandbox example for my problem: https://codesandbox.io/s/react-form-submit-problem-qn0de. Please try to click the "+"/"-" button on both Function Example and Class Example and you'll see the difference. On the Function Example, we always get the previous value while submitting.
I'll explain details about this example below.
We have a react component like this
function Counter(props) {
return (
<>
<button type="button" onClick={() => props.onChange(props.value - 1)}>
-
</button>
{props.value}
<button type="button" onClick={() => props.onChange(props.value + 1)}>
+
</button>
<input type="hidden" name={props.name} value={props.value} />
</>
);
}
It contains two buttons and a numeric value. User can press the '+' and '-' button to change the number. It also renders an input element so we can use it in a <form>.
This is how we use it
class ClassExample extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 1,
lastSubmittedQueryString: ""
};
this.formEl = React.createRef();
}
handleSumit = () => {
if (this.formEl.current) {
const formData = new FormData(this.formEl.current);
const search = new URLSearchParams(formData);
const queryString = search.toString();
this.setState({
lastSubmittedQueryString: queryString
});
}
};
render() {
return (
<div className="App">
<h1>Class Example</h1>
<form
onSubmit={event => {
event.preventDefault();
this.handleSumit();
}}
ref={ref => {
this.formEl.current = ref;
}}
>
<Counter
name="test"
value={this.state.value}
onChange={newValue => {
this.setState({ value: newValue }, () => {
this.handleSumit();
});
}}
/>
<button type="submit">submit</button>
<br />
lastSubmittedQueryString: {this.state.lastSubmittedQueryString}
</form>
</div>
);
}
}
We render our <Counter> component in a <form>, and want to submit this form right after we change the value of <Counter>. However, on the onChange event, if we just do
onChange={newValue => {
this.setState({ value: newValue });
this.handleSubmit();
}}
then we won't get the updated value, probably because React doesn't run setState synchronously. So instead we put this.handleSubmit() in the second argument callback of setState to make sure it is executed after the state has been updated.
But in the Function Example, as far as I know in state hooks there's nothing like the second argument callback function of setState. So we cannot achieve the same goal. We found out two workarounds but we are not satisfied with either of them.
Workaround 1
We tried to use the effect hook to listen when the value has been changed, we submit our form.
React.useEffect(() => {
handleSubmit();
}, [value])
But sometimes we need to just change the value without submitting the form, we want to invoke the submit event only when we change the value by clicking the button, so we think it should be put in the button's onChange event.
Workaround 2
onChange={newValue => {
setValue(newValue);
setTimeout(() => {
handleSubmit();
})
}}
This works fine. We can always get the updated value. But the problem is we don't understand how and why it works, and we never see people write code in this way. We are afraid if the code would be broken with the future React updates.
Sorry for the looooooooong post and thanks for reading my story. Here are my questions:
How about Workaround 1 and 2? Is there any 'best solution' for the Function Example?
Is there anything we are doing wrong? For example maybe we shouldn't use the hidden input for form submitting at all?
Any idea will be appreciated :)
Can you call this.handleSubmit() in componentDidUpdate()?
Since your counter is binded to the value state, it should re-render if there's a state change.
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
this.handleSubmit();
}
}
This ensure the submit is triggered only when the value state change (after setState is done)
It's been a while. After reading React 18's update detail, I realize the difference is caused by React automatically batching state updates in function components, and the "official" way to get rid of it is to use ReactDOM.flushSync().
import { flushSync } from "react-dom";
onChange={newValue => {
flushSync(() => {
setValue(newValue)
});
flushSync(() => {
handleSubmit();
});
}}

React not rendering all elements within a list of components

I have a system in which when the user presses a button, a new component is pushed to a list of components and the state is updated. I render the list of components using {}, but only the first element is rendered.
I've used console.log to ensure that my list is actually updating. All the questions I've seen so far for this problem involve using a class that extends React.Component. Since I'm using a function to render, I don't see how I can use those solutions
export default function ExampleManager() {
const [examples, setExamples] = React.useState([<Example key={0}/>);
function handleClick() {
examples.push(<Example key={examples.length}/>);
setExamples(examples);
}
return (
<>
{examples}
<Button variant="outlined" color="primary" onClick={handleClick}>
Add Example
</Button>
</>
);
}
If the button was to be clicked multiple times, I would expect there to be multiple Example components, however at the moment only the first element works
As far as I know, examples is immutable and isn't updated by using examples.push().
Change your handleClick to the following code, to remove the reference of your example variable:
function handleClick() {
// create a new array to prevent referencing the old on
setExamples([
...examples,
<Example key={examples.length}/>
]);
}
Nonetheless you shouldn't add components per se into your array. Try to split values and its representation like the following:
function ExampleManager() {
const [examples, setExamples] = React.useState([0]);
const handleClick = () => setExamples([...examples, examples.length])
return (
<>
{examples.map((item, key) => <Example key={key} data={item} />)}
<Button variant="outlined" color="primary" onClick={handleClick}>
Add Example
</Button>
</>
)
}

why <button onClick={this.props.onClick}> is different from <button onClick={(e) => {this.props.onClick(e)}}> here?

I found the following code in this post.(sandbox) I am not sure why these buttons behave differently.
One possible explanation is: Because Button's render method is not invoked for updating, the first button's click handler remains the same. However, this.props of the Button instance has changed to include the new onClick prop. In other words,if the props of a component element is changed, nextProps will finally become this.props even when shouldComponentUpdate returns false.
const submit = val => alert(val);
class App extends React.Component {
state = { val: "one" }
componentDidMount() {
this.setState({ val: "two" })
}
render() {
return <Form value={this.state.val} />
}
}
const Form = props => (
<Button
onClick={() => {
submit(props.value)
}}
/>
)
class Button extends React.Component {
shouldComponentUpdate() {
// lets pretend like we compared everything but functions
return false
}
handleClick = () => this.props.onClick()
render() {
return (
<div>
<button onClick={this.props.onClick}>This one is stale</button>
<button onClick={() => this.props.onClick()}>This one works</button>
<button onClick={this.handleClick}>This one works too</button>
</div>
)
}
}
Your explanation is correct on that when ShouldComponentUpdate() returns false, the Button component does not re-render on props change.
In the first <button> element, the onClick event is this.props.OnClick which is actually the function () => { submit("one"); } at the moment of the initial rendering. "one" is here because at this particular time point in the React lifecycle, Form's props.value evaluates to "one". Note that the function gets executed has nothing to do with Button's props.
In the second <button> element, however, the onClick event is () => this.props.onClick(). Regardless whether the Button component gets re-rendered, it is always the this.props.onClick() that get executed, where this.props changes when the parent components get state/props updates.
In the case of ShouldComponentUpdate() returning true, Button component will re-render on props updates. The first button's onClick event becomes a new function () => { submit("two"); } on the re-render triggered by the App component's state change. The two button elements' onClick event functions are never the same function, although they appear to produce the same result.
<button onClick={this.props.onClick}> Will call your onClick function and send an event object as its first parameter. This is the equivalent of doing this :
<button onClick={event => { this.props.onClick(event) }}>
<button onClick={() => this.props.onClick()}> Will set what your onClick function returns as the onClick function... It is the short syntax of the following code :
<button onClick={() => { return this.props.onClick() }}>
What you may be looking for is this : <button onClick={() => { this.props.onClick() }}>
In this case, clicking will simply call your function without sending any arguments
I suggest reading this documentation to know more about arrow/anonymous functions and their syntax

React - Change Button HTML to loader on click

I have an Alerts component which is responsible for rendering alerts from JSON supplied to it's props:
alert.js (cut down for brevity)
createAlert(alerts) {
return alerts.map(content => (
<Col key={content.ID} s={12}>
<div className="chip">
<Icon className="alert">error_outline</Icon>
<p>{content.Comment}</p>
<span onClick={() => this.props.onRead(content.ID)}>
<Icon className="closeAlert">close</Icon>
</span>
</div>
</Col>
));
}
render() {
let content = {};
if (!this.props.content) {
//Alerts are null so they are still loading.. show loader
content = this.createLoader();
} else if (!this.props.content.success){
//Error getting alerts
content = this.createAlertError(this.props.content.error);
}
else if (this.props.content.alerts.length === 0) {
//Alert array is null, therefor no alerts
content = this.createNoAlerts();
} else {
//Render Alerts
content = this.createAlert(this.props.content.alerts);
}
return <div>{content}</div>;
}
}
In the above snippit, you can see that if
this.props.alerts
is an array with elements, then it will run
createAlert()
which will create an array of React Components (in this case its just React-Materialize component which is just a <div></div>)
the part I am interested in is the span with the onClick event
<span onClick={() => this.props.onRead(content.ID)}>
<Icon className="closeAlert">close</Icon>
</span>
This run an event from the parent component.
The method that is run in the parent is as follows:
alertRead(id) {
this.props.deleteAlert(id);
}
What I would like, is some way to add a spinning loader icon into the button on the click, in jQuery it would be:
$(button).on("click", function(){
this.html("<i class='fa fa-spin fa-spinner'></i>"); //Spinner Icon
});
The question is, how do I edit the HTML of the button that is clicked on click?
No Redux version
I don't see any redux relation in the code so I will assume that you are not using it or not using it in this particular flow.
What you need to do is to add state to the Alert component and do two things in onClick handler:
() => { this.props.onRead(content.ID); this.setState({clicked: true});}
Of course you need to have state initialization with clicked: false. Second thing is to use this state in rendering:
{this.state.clicked && <Loader />}
{!this.state.clicked && <YourButton />}
So when clicked show loader when not clicked show button. Above code examples are only for showing you the right path.
Version assuming of Redux using.
If you are using redux then alert needs to have connection with the store like:
connect((state) => ({ isClicked: getIsButtonClicked(state)}), { dispatchClick })(AlertComponent)
And you need to dispatch click action after click ( it will set the store state responsible for that - clicked on true.
() => { this.props.onRead(content.ID); this.props.dispatchClick();}
Also finnaly you need to use this prop in rendering:
{this.props.isClicked && <Loader />}
{!this.props.isClicked && <YourButton />}
This solution will cover all instances of Alert component. I am not covering here the redux part.

Resources