React setState() needs 2 clicks to update UI - javascript

I'm new in React. My question may be common in React developers and there are many same questions but I still don't know how to resolve that. I must still click twice to update UI state. The first click just calls event handler but not update counter variable in state. Even I used the callback form of setState() like the following:
this.setState({ hasButtonBeenClicked: true }, () => {console.log("Clicked")});
the console.log("Clicked") was not reached in first click as well!
App.js
import React, { Component, useState } from "react";
import { Summary } from "./Summary";
import ReactDOM from "react-dom";
let names = ["Bob", "Alice", "Dora"]
function reverseNames() {
names.reverse();
ReactDOM.render(<App />, document.getElementById('root'));
}
function promoteName(name) {
names = [name, ...names.filter(val => val !== name)];
ReactDOM.render(<App />, document.getElementById('root'));
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
incrementCounter = (increment) => this.setState({counter: this.state.counter + increment});
render() {
return (
<table className="table table-sm table-striped">
<thead>
<tr><th>#</th><th>Name</th><th>Letters</th></tr>
</thead>
<tbody>
{names.map((name, index) =>
<tr key={name}>
<Summary index={index} name={name}
reverseCallback={() => reverseNames()}
promoteCallback={() => promoteName(name)}
counter={this.state.counter}
incrementCallback={this.incrementCounter}
/>
</tr>
)}
</tbody>
</table>
)
}
}
Summary Component
import React, { Component } from "react";
import { SimpleButton } from "./SimpleButton";
export class Summary extends Component {
render() {
const props = this.props;
return (
<React.Fragment>
<td>{props.index + 1} </td>
<td>{props.name} </td>
<td>{props.name.length} </td>
<td>
<SimpleButton
className="btn btn-warning btn-sm m-1"
callback={() => props.reverseCallback()}
text={`Reverse (${props.name})`}
{...this.props}
/>
</td>
</React.Fragment>
)
}
}
SimpleButton
import React, { Component } from "react";
export class SimpleButton extends Component {
constructor(props) {
super(props);
this.state = {
hasButtonBeenClicked: false
}
}
handleClick = (e) => {
this.props.incrementCallback(3);
this.setState({ hasButtonBeenClicked: true });
this.props.callback();
console.log(e);
}
render() {
return (
<button onClick={(e) => this.handleClick(e)}
className={this.props.className}
disabled={this.props.disabled === "true"
|| this.props.disabled === true}>
{ this.props.text} { this.props.counter}
{ this.state.hasButtonBeenClicked &&
<div>Button Clicked!</div>
}
</button>
)
}
}

I resolved the problem by commenting out the line in App.js
function reverseNames() {
names.reverse();
// ReactDOM.render(<App />, document.getElementById('root'));
}
I thinks the line making app rerender before the actual state updated so I was behind the actual state 1 span. The first click is the initial state, the second click is the state after the first click .etc

Related

React component that's rendered dynamically does not rerender on parent state changes

I have a component that I want to run through a non react animation library before render. This has prevented me from going the standard route of just using the standard hide/show logic. I initially tried to use ReactDOM.createPortal but that didn't render the component at all. Using ReactDOM.render, I've gotten the element to render correctly upon completion of the animation and I'm able to successfully propagate changes up to the "parent" state but the state change doesn't propagate back down to the "child". Here's my code:
Html
<div id="root"></div>
<div id="childPlaceholder"></div>
Javascript
import './App.css';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
function App() {
const [data, updateData] = useState(0)
function add(val) {
console.log("add");
updateData(val);
}
function renderSubpage() {
let $el = document.getElementById("childPlaceholder");
// NonReactAnimationLibrary.ShowContainer($el);
ReactDOM.render(<Child number={data} add={add} />, $el);
// ReactDOM.createPortal(<Child number={data} add={add} />, $el);
}
return ( <>
<button onClick={renderSubpage}>
add child
</button>
<div> data: {data}</div>
</>
);
}
function Child(props) {
return <>
<button onClick={()=>{props.add(props.number + 1)}}>add number</button>
<div>child {props.number}</div>
</>
}
export default App;
Is it possible to do this in react?
Update 1:
So I've updated the code per Olivers response, it renders correctly using the portal but the child components still don't rerender on state changes in the Parent Component
const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");
function Child(args) {
return ReactDOM.createPortal(<>
<div>child: {args.number}</div>
<button onClick={()=>{args.add(args.number+1)}}>Increment base number</button>
</>, childRoot);
}
export default class App extends React.Component {
constructor() {
super();
this.state = { data: 0, number:0 };
}
add = (val)=> {
this.setState({
...this.state,
number: val
});
}
addChild = () => {
this.setState(prevState => ({data: prevState.data + 1}));
}
render() {
const children = Array(this.state.data)
.fill()
.map((_, i) => <Child key={i} number={0} add={this.add}/>);
return (
<div>
<button onClick={this.addChild}>
add child
</button>
<div> data: {this.state.data}</div>
{children}
</div>
);
}
}
ReactDOM.render(<App/>, root);
Update 2:
The culprit was found. Changed
number={0}
to
number={this.state.number}
and it works
React.createPortal must be used inside the render method (I used a class component because I cannot use hooks in the SO example, you can of course use a functional component).
You can use it in the App component like below or in the Child component :
const root = document.getElementById("root");
const childRoot = document.getElementById("childPlaceholder");
function Child({number}) {
return <div>child {number}</div>;
}
class App extends React.Component {
constructor() {
super();
this.state = { data: 0 };
}
addChild = () => {
this.setState(prevState => ({data: prevState.data + 1}));
}
render() {
const children = Array(this.state.data)
.fill()
.map((_, i) => <Child key={i} number={i} />);
return (
<div>
<button onClick={this.addChild}>add child</button>
<div> data: {this.state.data}</div>
{ReactDOM.createPortal(children, childRoot)}
</div>
);
}
}
ReactDOM.render(<App/>, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="childPlaceholder"></div>

react does not update DOM

import React, { Component } from "react";
import ReactDOM from "react-dom";
import "./index.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
listItem: []
}
this.onChangeInput = this.onChangeInput.bind(this);
this.addToList = this.addToList.bind(this);
this.keyPress = this.keyPress.bind(this);
}
onChangeInput(event) {
this.setState({
text: event.target.value
});
}
addToList () {
let list = this.state.listItem;
list.push(this.state.text);
this.setState({
text: ""
});
this.setState({
listItem: list
});
}
deleteItem(event) {
console.log(event.target.remove());
}
keyPress (e) {
if (e.key === "Enter") {
this.addToList()
}
}
render() {
const listItem = this.state.listItem;
const list = listItem.map((val, i) =>
<li key={i.toString()} onClick={this.deleteItem}>
{val}
</li>
);
console.log(list);
return (
<div className="container">
<Input onChange={this.onChangeInput} value={this.state.text}
keyPress={this.keyPress}
/>
<Button addToList={this.addToList}/>
<ul>
{list}
</ul>
</div>
);
}
}
class Input extends Component {
render() {
return <input type="text" className="input" onChange={this.props.onChange}
onKeyPress={this.props.keyPress}
value={this.props.value}/>;
}
}
class Button extends Component {
render() {
return (
<button className="button" onClick={this.props.addToList}>
Add To List
</button>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
I'm very confused and couldn't find solution any where.
I'm new to react.
when I delete the list items, I delete them from DOM but is in state and I didn't delete it from state.
I put console.log(list) in render method and on every key press logs list in console
my question is why DOM does not re-render lists and output those where deleted from DOM and not from state?
and why it works for new list items and ignore those that deleted from DOM ?
react dosent pickup the update in the way you are doing it
deleteItem(event) {
console.log(event.target.remove());
}
although the item will be removed , but react dosent have any clue that happend, to notify react that the items has changed and it need to re-render, you need to call setState , then react calls the render method again,
deleteItem(e) {
const list= this.state.listItem;
list.pop() // remove the last element
this.setState({
list: list
});
}

Attribute ref return null in React

Trying to get a reference to a DOM element in react is returning null, I have used selector methods and ref yet all returned null. The code those work well, as the dropdown is created, but the problem is:
while i click on the document or the button for the second time, i get an error message saying this.containerElem is null.
Any help will be appreciated.
This is my code
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class DropdownMenu extends React.Component {
constructor() {
super();
this.state = {
isToggle: false,
};
}
handleClick(e) {
e.preventDefault();
this.setState({
isToggle: !this.state.isToggle,
}, () => document.addEventListener('click', this.closeMenu.bind(this))
);
}
closeMenu(ev) {
if (!this.containerElem.contains(ev.target)) {
this.setState({
isToggle: !this.state.isToggle
}, () => document.removeEventListener('click', this.closeMenu))
}
}
render() {
return (
<div>
<ClickBtn onHandleClick={this.handleClick.bind(this)} />
{
(this.state.isToggle) ? (
<div className="menu"
ref={(node) => {
this.containerElem = node;
}}
>
<button>Menu Item 1</button>
<button>Menu Item 1</button>
<button>Menu Item 1</button>
</div>
) :
(
null
)
}
</div>
)
}
}
function ClickBtn(props) {
return (
<button type="button" onClick={props.onHandleClick}>Click to Toggle</button>
)
}
ReactDOM.render(
<DropdownMenu />, document.getElementById('root')
);

How to pass props in my components inReactJS

Hello I have a collapse wrapper which has its Display state as true by default (I can't change that)
And a modal component which needs the display False when it opens.
I thought that setting the defaultOpen props as "false" would set the Display to false. But it doesn' work.
What do I do wrong ?
Here is the code :
My Collapse wrapper :
import React, { Component } from "react";
import ChevronUp from "react-feather/dist/icons/chevron-up";
import ChevronDown from "react-feather/dist/icons/chevron-down";
import { Collapse } from "reactstrap";
import "./style.scss";
class CollapseWrapper extends Component {
constructor(props) {
super(props);
this.state = {
display:
props.defaultOpen === undefined ? true : props.defaultOpen,
title: this.props.title,
};
}
toggleContainer = () => {
this.setState(prevState => ({
display: !prevState.display,
}));
};
render() {
const { display, title } = this.state;
return (
<div>
<button type="button" onClick={this.toggleContainer}>
<div className="title-container">
{display ? (
<ChevronUp className="chevron" />
) : (
<ChevronDown className="chevron" />
)}
<h2>{title}</h2>
</div>
</button>
<Collapse isOpen={this.state.display}>
{this.props.children}
</Collapse>
</div>
);
}
}
export default CollapseWrapper;
My modal :
import React from "react";
import { Modal } from "reactstrap";
import CollapseWrapper from "./CollapseWrapper";
class Mymodal extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { isOpen } = this.props;
return (
<Modal size="xxl" isOpen={isOpen} toggle={this.close}>
<CollapseWrapper defaultOpen="false" title="More détails">
Some content...
</CollapseWrapper>
</Modal>
);
}
}
export default Mymodal;
You should pass boolean value inside the curly braces {} not in string.
Correct defaultOpen={false}
wrong defaultOpen="false"
<CollapseWrapper defaultOpen={false} title="More détails">
Some content...
</CollapseWrapper>
use arrow function in onClick event on your button
replace
onClick={this.toggleContainer}
to
onClick={() => this.toggleContainer()}
I think, you can use component life cycle method componentWillReceiveProps(nextProps) to solve this issue. Set your display state again when componentWillReceiveProps.
Solution:
componentWillReceiveProps(nextProps) {
if (nextProps.defaultOpen !== this.state.display) {
this.setState({ ...this.state, display: nextProps.defaultOpen });
}
}

Countdown timer multiple item

I'm using this package called react-countdown-now I have many items I'm just wondering why all the items counting down same time
https://codesandbox.io/s/4rpn84j5m0
As you can see below in my example the new item rest the old item
import React, { Component } from 'react';
import { render } from 'react-dom';
import Countdown from 'react-countdown-now';
class App extends Component {
constructor() {
super();
this.state = {
show: false
};
}
componentDidMount() {
setTimeout(() => {
this.setState({ show: true })
}, 2500)
}
render() {
return (
<div>
<Countdown date={Date.now() + 10000} /><br />
{this.state.show ? <Countdown date={Date.now() + 10000} /> : <span>waiting new item....</span>}
<p>
Why the new item rest the old item?
</p>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Why the new item rest the old item?
The reason is because the expression Date.now() + 10000 gets recalculated on every re-render / state update. You need to store the dates somewhere.
As an example:
https://codesandbox.io/s/zq1mv4zmz4
import React, { Component } from "react";
import { render } from "react-dom";
import Countdown from "react-countdown-now";
class App extends Component {
constructor() {
super();
this.state = {
dates: [Date.now() + 10000]
};
}
componentDidMount() {
setTimeout(() => {
this.setState({ show: true });
}, 2500);
}
render() {
return (
<div>
{this.state.dates.map(date => (
<div>
<Countdown date={date} />
</div>
))}
<button onClick={() => this.addDate()}>New Item</button>
<p>Why the new item rest the old item?</p>
</div>
);
}
addDate() {
let dates = [...this.state.dates, Date.now() + 10000];
this.setState({ dates });
}
}
render(<App />, document.getElementById("root"));

Categories

Resources