Get updated state of the component outside the render - reactjs

Sorry, this could be very basic, but I've this sutuation...
class MyComponent extends React.Component {
constructor () {
super()
this.state = {
selected: -1
}
}
this.list = []
for (let i = 0; i < myArray.length; i++) {
let item = <div>selected on this list : {this.state.selected}</div>
this.list.push(item)
}
render () {
return (
<div>selected: {this.state.selected}</div>
<div onClick={() => this.state.selected++}>
{this.list}
</div>
}
}
This supposed to generate:
selected : -1
on this list: -1
on this list: -1
on this list: -1
on this list: -1
...
Basically all things returning the value of the selected.
But when I click on the div to increment the selected, I'm receiving something like...
selected : 2
on this list: -1
on this list: -1
on this list: -1
on this list: -1
...
and as longs it's updates...
selected : 3,4,5,6,7...
on this list: -1
on this list: -1
on this list: -1
on this list: -1
...
In other words: the values of my list created by the for/loop is always displaying the initial state while the first div inside the render shows the state properly updated.
Any help ?

You have made a couple of mistakes:
You use the state of component directly to update it's value. You should use setState function (read here)
You store components in variable (list). Instead of doing this, you should store only data and render it in render function, or separate component.
Try not to use for loops. React behaves more predictably when you use map and other Array prototype methods.
If you want to handle state changes outside the render see compinentWillUpdate and componentDidUpdate methods.
Working example (fiddle):
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
selected: -1
}
this.handleOnClick = this.handleOnClick.bind(this);
}
render() {
const selected = this.state.selected;
const myArray = [1, 2, 3, 4];
return (
<div>
<div>selected: {selected}</div>
<div onClick={this.handleOnClick}>
{myArray.map((_, index) => <div key={index}>selected on this list: {selected}</div>)}
</div>
</div>
)
}
handleOnClick() {
const selected = this.state.selected;
this.setState({ selected: selected + 1 });
}
}

The main issue is that you're mutating your state instead of using setState (https://facebook.github.io/react/docs/react-component.html)
This would be an example:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
selected: -1
};
}
updateSelected = () => {
this.setState({
selected: this.state.selected + 1
});
};
render() {
let list = [];
//do whatever you need with list
return (
<div>
<div>selected: {this.state.selected}</div>
<div onClick={this.updateSelected}>
{list}
</div>
</div>)
}
}

Because you are storing the ui-items inside a global variable and that is not getting updates, for loop will run only once during the initial rendering so ui items stored in global variable will have the old state value, another thing is the way you are updating the state value is not correct.
Changes:
1. Never mutate this.state directly, Use setState to update the state value.
2. Storing the ui component in state variable or in global variable is not a good idea, all ui part must be inside render method.
3. Instead of using arrow function inside render to handle events, bind the method in constructor for the performance reason.
4. Assign unique key to element inside loop, check the DOC for details.
Working Code:
class MyComponent extends React.Component {
constructor () {
super()
this.state = {
selected: -1
}
this._handleClick = this._handleClick.bind(this);
}
_handleClick(){
this.setState(prevState => ({
selected: prevState.selected + 1,
}))
}
render () {
let items = [];
for (let i = 0; i < ([1,2,3,4,5]).length; i++) {
items.push(<div key={i}>selected on this list : {this.state.selected}</div>)
}
return (
<div>
<div onClick={ this._handleClick }>Increment + </div>
<div>selected: {this.state.selected}</div>
{items}
</div>
)
}
}
ReactDOM.render(<MyComponent/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'/>

Related

React - Class component doesn't receive any params

I have a simple class Component:
class SearchedUserWrapper extends Component {
constructor(props) {
super(props);
this.state = {
searchedPhrase: "",
pageNumber: 1
};
this.increment = this.increment.bind(this);
}
GetUserForSearchResult = (postAmount, pageNumber) => {
const list = [];
for (let index = 0; index < postAmount; index++) {
list.push(<SearchResultUser CurrentPage={pageNumber}></SearchResultUser>);
}
return list;
}
increment = () => {
this.setState({ pageNumber: this.state.pageNumber + 1 })
console.log(this.state.pageNumber + 0);
}
render() {
return (<div>
{this.GetUserForSearchResult(5, this.props.pageNumber)}
<Button onClick={this.increment}> Current page {this.state.pageNumber}</Button>
</div>);
}
}
and function GetUserForSearchResult receives a state from SearchUserWrapper class. My SearchResultUser looks like this:
class SearchResultUser extends Component {
render() {
{console.log(this.props.CurrentPage)}
return (
<div className="user-searchresult">
{this.props.CurrentPage}
</div>);
}
}
export default SearchResultUser;
And console log says that this props are undefined, and the div is empty.
My goal is to have the effect that everytime I click "Current page" button, to refresh all the SearchResultUser component so that it displays a state passed as parameter. What am I doing here wrong? Why does it says to be undefined?
EDIT:
I tried couple of things and discovered something.
If I send the state in the params directly, for example:
render() {
return (<div>
<SearchResultUser CurrentPage={this.state.pageNumber}></SearchResultUser>
</div>
It seems to work, but the order of sending the state to the function, which passes it to params of component doesn't work.
GetUserForSearchResult = (postAmount, pageNumber) => {
const list = [];
for (let index = 0; index < postAmount; index++) {
list.push(<SearchResultUser CurrentPage={pageNumber}></SearchResultUser>);
}
return list;
}
render() {
return (<div>
<SearchResultUser CurrentPage={this.state.pageNumber}></SearchResultUser>
</div>);
Can somebody explain why is it happening like this?
Edit:
In this place(below) I think you have to pass state instead of props, because the main component(SearchedUserWrapper) doesn't receive any props, so this is undefined.
{this.GetUserForSearchResult(5, this.props.pageNumber)}
https://codesandbox.io/s/stackoverflow-71594634-omvqpw
First message:
Did you check if the page number was updated?
If the next state depends on the current state, the doc of react recommend using the updater function form, instead:
this.setState((state) => {
return {quantity: state.quantity + 1};
});
You should call super(props) before any other statement. Otherwise, this.props will be undefined in the constructor at SearchResultUser

DOM Not Updating although function changes var

Even though the variable value changes to a value that would cause the element not to render, the page does not update and the element remains rendered.
Tried moving inside component, did not work.
function clickHandler(item)
{
object[item].active = 0;
}
let object = [{data:
<p onClick={() => clickHandler(0)}> Data </p>,
active:1},
{data:
<p onClick={() => clickHandler(1)}> Data2 </p>,
active:1}
];
class Objects extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div class="notifications">
{object[0].active == 1 ? object[0].data : " "}
{object[1].active == 1 ? object[1].data : " "}
</div>
);
}
}
ReactDOM.render(<Objects />, document.querySelector('#object_display'));"
Expects to disappear but it does not.
Changing external data isn't going to trigger an update of your component. You need to either change the props passed to the component or keep track of it in state inside the component itself.
Consider this:
// data declared outside the component; gets passed as a prop
// in the ReactDOM.render call below.
const data = [
{
title: "Object 1"
},
{
title: "Object 2"
},
{
title: "Object 3"
},
]
class Objects extends React.Component {
// initial state; start with the first item
state = {index: 0}
// onClick handler
switch = () => {
// get the current index out of this.state
const {index} = this.state;
// get the number of items in data so
// we can loop back to 0 when we get to
// the last item
const {data: {length}} = this.props;
// increment the index, don't go beyond length
const newIndex = (index + 1) % length;
// calling setState triggers a re-render
// with the new index value
this.setState({index: newIndex});
}
render () {
const {data} = this.props;
const {index} = this.state;
const item = data[index];
return (
<div onClick={this.switch}>{item.title} (Click for next item)</div>
);
}
}
// data passed as a prop
ReactDOM.render(<Objects data={data} />, document.getElementById('app'))
<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="app" />

How to scroll to bottom when props changed in react-virtualized?

I have component App with List from react-virtualized library.
And I need on initial render, that my List scroll to bottom.
And I did it, when added scrollToIndex option. But when I add new object in my list array, it does not scroll to my last added object. How can I fix it? And is it good solution to use "forceUpdate()" function?
import { List } from "react-virtualized";
import loremIpsum from 'lorem-ipsum';
const rowCount = 1000;
const listHeight = 600;
const rowHeight = 50;
const rowWidth = 800;
class App extends Component {
constructor() {
super();
this.renderRow = this.renderRow.bind(this);
this.list = Array(rowCount).fill().map((val, idx) => {
return {
id: idx,
name: 'John Doe',
image: 'http://via.placeholder.com/40',
text: loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound: 4,
sentenceUpperBound: 8
})
}
});
}
handle = () => {
this.list = [...this.list, { id: 1001, name: "haha", image: '', text: 'hahahahahaha' }];
this.forceUpdate();
this.refs.List.scrollToRow(this.list.length);
};
renderRow({ index, key, style }) {
console.log('____________', this.list.length);
return (
<div key={key} style={style} className="row" >
<div className="image">
<img src={this.list[index].image} alt="" />
</div>
<div onClick={this.handle}>{this.state.a}</div>
<div className="content">
<div>{this.list[index].name}</div>
<div>{this.list[index].text}</div>
</div>
</div>
);
}
render() {
return (
<div className="App">
<div className="list">
<List
ref='List'
width={rowWidth}
height={listHeight}
rowHeight={rowHeight}
rowRenderer={this.renderRow}
rowCount={this.list.length}
overscanRowCount={3}
scrollToIndex={this.list.length}
/>
</div>
</div>
);
}
}
export default App;
You mentioning you need to scroll to the bottom when the list item is changed and to be honest i don't like to use forceUpdate. As mentioned on the React docs:
Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render().
Luckily, one of React lifecycle method is suitable for this case, it is call componentDidUpdate. But you need to do some refactor of your code. Instead using private field, i suggest to put it on state/props.
This method will invoked immediately after updating props/state occurs. However, This method is not called for the initial render.
What you need to do is, compare the props, is it change or not? Then call this.refs.List.scrollToRow(this.list.length);
Sample code
class App extends Component {
constructor() {
this.state = {
list: [] // put your list data here
}
}
// Check the change of the list, and trigger the scroll
componentDidUpdate(prevProps, prevState) {
const { list } = this.state;
const { list: prevList } = prevState;
if (list.length !== prevList.length) {
this.refs.List.scrollToRow(list.length);
}
}
render() {
// usual business
}
}
more reference for React lifecyle methods:
https://reactjs.org/docs/react-component.html#componentdidupdate

Trigger Re-Render of Child component

I'm new to React and am running into the same problem a few times. In this particular situation, I'm trying to get an option in a select dropdown to update when I update a text input.
I have a parent, App, with the state attribute "directions", which is an array. This gets passed as a property to a child, GridSelector, which creates the text field and dropdown. When the text field is changed, a function triggers to update the parent state. This in turn causes the GridSelector property to update. However, the dropdown values, which are originally generated from that GridSelector property, do not re-render to reflect the new property value.
I'm trying to figure out the most React-ful way to do this and similar manuevers. In the past, I've set a state in the child component, but I think I've also read that is not proper.
My working site is at amaxalaus.bigriverwebdesign.com
Here's the pertinent code from each file:
App.js
class App extends React.Component {
constructor(props){
super(props);
this.state = {
directions: [],
dataRouteDirections: '/wp-json/wp/v2/directions',
currentDirectionsIndex: 0
}
this.addImageToGrid = this.addImageToGrid.bind(this);
this.changeTitle=this.changeTitle.bind(this);
}
componentDidMount(){
fetch(this.state.dataRouteDirections)
.then(data => data=data.json())
.then(data => this.setState({directions:data}));
}
addImageToGrid(image) {
this.refs.grid.onAddItem(image); //passes image add trigger from parent to child
}
createNewDirections(){
var directions= this.state.directions;
var index = directions.length;
var lastDirections = directions[directions.length-1];
var emptyDirections= {"id":0,"acf":{}};
emptyDirections.acf.grid="[]";
emptyDirections.acf.layout="[]";
emptyDirections.title={};
emptyDirections.title.rendered="New Directions";
if (lastDirections.id!==0 ) { ///checks if last entry is already blank
this.setState({
directions: directions.concat(emptyDirections), //adds empty directions to end and updates currentdirections
currentDirectionsIndex: index
});
}
}
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
render() {
var has_loaded; //has_loaded was added to prevent double rendering during loading of data from WP
this.state.directions.length > 0 ? has_loaded = 1 : has_loaded = 0;
if (has_loaded ) {
/* const currentGrid = this.state.directions;*/
return ( //dummy frame helpful for preventing redirect on form submit
<div>
<div className="fullWidth alignCenter container">
<GridSelector
directions={this.state.directions}
currentDirectionsIndex={this.state.currentDirectionsIndex}
changeTitle={this.changeTitle}
/>
</div>
<Grid ref="grid"
currentGrid={this.state.directions[this.state.currentDirectionsIndex]}
/>
<ImageAdd addImageToGrid={this.addImageToGrid}/>
<div className="fullWidth alignCenter container">
<button onClick={this.createNewDirections.bind(this)}> Create New Directions </button>
</div>
</div>
)
} else {
return(
<div></div>
)
}
}
}
GridSelector.js
class GridSelector extends React.Component {
constructor(props) {
super(props);
var currentDirections = this.props.directions[this.props.currentDirectionsIndex];
this.state = {
currentTitle:currentDirections.title.rendered
}
}
createOption(direction) {
if (direction.title) {
return(
<option key={direction.id}>{direction.title.rendered}</option>
)
} else {
return(
<option></option>
)
}
}
handleChangeEvent(val) {
this.props.changeTitle(val); //triggers parent to update state
}
render() {
return(
<div>
<select name='directions_select'>
{this.props.directions.map(direction => this.createOption(direction))}
</select>
<div className="fullWidth" >
<input
onChange={(e)=>this.handleChangeEvent(e.target.value)}
placeholder={this.state.currentTitle}
id="directionsTitle"
/>
</div>
</div>
)
}
}
You made a very common beginner mistake. In React state should be handled as an immutable object. You're changing the state directly, so there's no way for React to know what has changed. You should use this.setState.
Change:
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
To something like:
changeTitle(newTitle){
this.setState(({directions,currentDirectionsIndex}) => ({
directions: directions.map((direction,index)=>
index===currentDirectionsIndex? ({...direction,title:{rendered:newTitle}}):direction
})

React - child-parent relationship with props

I want to have a parent component that its children can register inside, so I can use this data somewhere else (generating a menu, for example).
Currently, my code is as follows:
const app = document.getElementById('app');
class Children extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.add(this.props.name);
}
render() {
return (
<div>Children</div>
)
}
}
class Items extends React.Component {
render() {
return (
<nav>
{this.props.content}
</nav>
)
}
}
class Parent extends React.Component {
constructor() {
super();
this.state = {
sections: []
}
}
add(section) {
const currentSections = this.state.sections;
const id = section.replace(' ', '-').toLowerCase();
const obj = { name: section, id };
this.setState({
sections: currentSections.push(obj)
});
}
render() {
console.log(this.state.sections);
return (
<div>
<Items content={this.state.sections} />
<Children add={this.add.bind(this)} name="Section 1" />
<Children add={this.add.bind(this)} name="Section 2" />
<Children add={this.add.bind(this)} name="Section 3" />
</div>
)
}
}
render(<Parent />, app);
My problem is, this.state.sections returns 3, but when I log it again in componentDidMount, it is an array.
What can I do?
JSBin
add(section) {
const currentSections = this.state.sections;
const id = section.replace(' ', '-').toLowerCase();
const obj = { name: section, id };
currentSections.push(obj)
this.setState({
sections: currentSections
});
}
Reason is you were setting state to currentSections.push(obj) which actually returns the count not array. Push earlier and set the sections as currentSections
The problem appears to be due to using push to append an item, which returns an index of the new element.
Mutating state in-place is not the best option, and I would argue it's better to use concat instead:
{
sections: currentSections.concat([obj])
}
Which will return a new array.
In your specific case, there's also going to be a race condition between the add calls: the three callbacks will be called at approximately the same time, so in all three, currentSections will be []. Then each one will append just on item and set it, and ultimately the state is going to end up containing only one element, not three.
To mitigate this, you can use another way of calling setState that ensures all three will be added sequentially:
this.setState(state => {
return {
sections: state.sections.concat([obj])
};
})

Resources