I am trying to implement minesweeper with React. I have a Header component (not shown) where the dimensions and the number of bombs can be changed so the App component state (sizeX, sizeY and bombNumber) will be updated, and these values will be sent down to the Table component as props.
My problem is that the Table component state is dependent on App component's props and when the createTable()method gets invoked on prop updates it uses the updated sizeX and sizeY props (which is good of course) but for some reason it uses the previous value of the bombNumber prop.
Is this the right approach? If so what did I miss?
class App extends Component {
constructor (props) {
super(props);
this.state = {
sizeX: DEFAULT_SIZE,
sizeY: DEFAULT_SIZE,
maxBombNumber: DEFAULT_BOMB_NUMBER,
bombNumber: DEFAULT_BOMB_NUMBER
}
updateSize (val, side) {
this.setState({[side]: val}, function () {
this.setState({maxBombNumber: this.calculateMaxBombNumber()}, function () {
if (this.state.maxBombNumber < this.state.bombNumber) {
this.setState({bombNumber: this.state.maxBombNumber}, function (){
});
}
});
});
}
render() {
return (
<div className="App">
<Table
sizeX={this.state.sizeX}
sizeY={this.state.sizeY}
bombNumber={this.state.bombNumber}
/>
</div>
);
}
}
Table.js
class Table extends Component {
constructor (props) {
super(props);
this.state = {
table: this.createTable()
}
componentWillReceiveProps(nextProps) {
this.setState({table: this.createTable()});
}
createTable() {
// function using sizeX, sizeY and bombNumber
}
}
Update:
The console log here prints the right values if I change the state, but the child will use the old value for bombNumber.
updateSize (val, side) {
this.setState({[side]: val}, function () {
this.setState({maxBombNumber: this.calculateMaxBombNumber()}, function () {
if (this.state.maxBombNumber < this.state.bombNumber) {
this.setState({bombNumber: this.state.maxBombNumber}, function (){
console.log(this.state);
});
}
});
});
}
Try to wrap all setState calls in one:
updateSize(val, side) {
const maxBombNumber = this.calculateMaxBombNumber();
const bombNumber = maxBombNumber < this.state.bombNumber ? maxBombNumber : this.state.bombNumber;
this.setState({
[side]: val,
maxBombNumber,
bombNumber
});
}
UPD:
componentWillReceiveProps(nextProps) {
this.setState({table: this.createTable()});
}
When Table component get new props from parent App –> componentWillReceiveProps execute and this.createTable() method use previous values not that in nextProps. In this way you should give it properly by arguments to createTable for example. Check this example.
UPD 2:
It will be better to make Table component – dump (stateless) and use state only in parent component App. Just move createTable method to App and it will be enough.
Or you can call createTable inside render method of Table.
I believe your issue is that setState is an asynchronous call, so the this.state that you're using is not updated. See this article for more information about using setState with a function. https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b
I think this would get you what you want:
updateSize(val, side) {
this.setState({ [side]: val }, function () {
this.setState({ maxBombNumber: this.calculateMaxBombNumber() }, this.setState(function (state) {
if (state.maxBombNumber < state.bombNumber) {
this.setState({ bombNumber: state.maxBombNumber }, function () {
console.log(state);
});
}
}));
});
}
Related
I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}
I'm having an issue where I want to save the data from a particular fieldset with the default values on componentDidMount().
The data saving happens in the parent component, after it is sent up from the child component. However, as React's setState() is asynchronous, it is only saving data from one of the fields. I have outlined a skeleton version of my problem below. Any ideas how I can fix this?
// Parent Component
class Form extends Component {
super(props);
this.manageData = this.manageData.bind(this);
this.state = {
formData: {}
}
}
manageData(data) {
var newObj = {
[data.name]: data.value
}
var currentState = this.state.formData;
var newState = Object.assign({}, currentState, newObj);
this.setState({
formData: newState, // This only sets ONE of the fields from ChildComponent because React delays the setting of state.
)};
render() {
return (
<ChildComponent formValidate={this.manageData} />
)
}
// Child Component
class ChildComponent extends Component {
componentDidMount() {
const fieldA = {
name: 'Phone Number',
value: '123456678'
},
fieldB = {
name: 'Email Address',
value: 'john#example.com'
}
this.props.formValidate(fieldA);
this.props.formValidate(fieldB)
}
render() {
/// Things happen here.
}
}
You're already answering you're own question. React handles state asynchronously and as such you need to make sure you use the current component's state when setState is invoked. Thankfully the team behind React is well-aware of this and have provided an overload for the setState method. I would modify your manageData call to the following:
manageData(data) {
this.setState(prevState => {
const nextState = Object.assign({}, prevState);
nextState.formData[data.name] = data.value;
return nextState;
});
}
This overload for the setState takes a function whose first parameter is the component's current state at the time that the setState method is invoked. Here is the link where they begin discussing this form of the setState method.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Change manageData to this
manageData(data) {
const newObj = {
[data.name]: data.value
};
this.setState(prevState => ({
formData: {
...prevState.formData,
...newObj
}
}));
}
I need to pass a valueindex from parent to child. Both parent and child need have function to revise index. When parent revise index, child can not get the update. is there anything I can fix it?
parent class:
constructor(props) {
super(props);
this.state = {
index: 0
}
}
parentFunction(dir) {
this.setState({
index: 10
});
}
render() {
return(
<Child index={this.state.index}/>
);}
childclass:
constructor(props) {
super(props);
this.state = {
index: this.props.index
};
}
childFunction(dir) {
this.setState({
index: this.props.index+1
});
}
render() {
return(
<div>{this.state.index}</div>
);}
You don't need to keep the updater function in both the class. You can pass the updater function from parent down to child and let the parent handle the state updates. Also setting a state based on props in constructor is an anti-pattern. You should use the prop in child directly for your usecase. In case you need to update the child state from props make sure to do that in the componentWillReceiveProps as well, since the constructor is only called the first time and the componentWillReceiveProps on every parent re-render
componentWillReceiveProps(newProps) {
if (newProps.index!== this.props.index) {
this.setState({
index:newProps.index
})
}
}
However what you need is
parent class:
constructor(props) {
super(props);
this.state = {
index: 0
}
}
parentFunction(dir) {
this.setState({
index: 10
});
}
updaterFunction(dir) {
this.setState((prevState) => ({
index: prevState.index+1
}));
}
render() {
return(
<Child updaterFunction={(val) => this.updaterFunction(val)} index={this.state.index}/>
);}
childclass:
updateProps = () => {
this.props.updaterFunction();
}
render() {
return(
<div>{this.props.index}</div>
);}
If you read the article about the lifecycle of react react life cycle then u will understand why,you pass the value from the parent to the children,but you didn't tell the react the it should be updated,add the code like beblow:
//add this life cycle in the child component
componentWillReceiveProps(newProps) {
if (newProps.index!== this.props.index) {
this.setState({
index:newProps.index
})
}
}
You have (at least) 2 options.
The best way to do this is to pass the parent function that changes
the index to the child via props. Then you can call that function in
the child class and it will change the parents state. In the render
method of the child class just use the index from props rather than
local state of the child.
The bad way to do this would be in the render method of child to
have a call to set state that sets state of index equal to the value
passed in props.
I need to get data from DB depending on a search string value. Therefore I'm using an input field. The search string is stored as a state value.
The data for the component comes from a container (using npm meteor/react-meteor-data).
Now my problem is, how do I get the search string into the container to set the parameter for the publication?
container/example.js
export default createContainer((prop) => {
Meteor.subscribe('images', searchString) // How to get searchString?
return { files: Images.find({}).fetch() }
}, Example)
component/example.jsx
class Example extends Component {
constructor(props) {
super(props)
this.state = {
searchString: ''
}
}
searchImage(event) {
const searchString = event.target.value
this.setState({ searchString })
}
render() {
return (<Input onChange={ this.searchImage.bind(this) }/>)
}
}
export default Example
publication
Meteor.publish('images', function(search) {
return Images.find({ title: search }).cursor
})
Maybe you can create two different components: a parent and a child, and you can wrap child component with createContainer HOC like the following
childComponent.js
const Example = (props) => {
return <Input onChange={props.searchImage}/>
}
export default createContainer(({searchString}) => {
Meteor.subscribe('images', searchString)
return { files: Images.find({}).fetch() }
}, Example)
parentComponent.js
class ExampleWrapper extends Component {
constructor(props) {
super(props)
this.state = {
searchString: ''
}
}
searchImage = (event) => {
const searchString = event.target.value
this.setState({ searchString })
} // instead of binding this, you can also use arrow function that
// takes care of binding
render() {
return (<Example searchImage={this.searchImage} searchString={this.state.searchString} {...this.props} />)
}
}
export default ExampleWrapper
The idea is, since createContainer is a higher order component, it doesn't have access to the props of any component wrapped by it.
What we need to do is, passing the value of searchString from a parent component.
The way to do is the following:
ExampleWrapper has a state called searchString and Example component has a prop called searchString. We can set the value of searchString prop to state.searchString.
Since the default export corresponds to createContainer({..some logic…}, Example}), createContainer can make use of prop called searchString.
In order to change the value of state.searchString we also passed searchImage function as a prop to Example component. Whenever there is a change event, onChange triggers searchImage function that updates the value of state.searchString. And eventually, the minute the value of state.searchString changes searchString prop’s value changes thus your subscription result also changes
onChange={ (e)=> {this.setState({ searchString: $(e.target).val() }) } }
This is how we assign values to our internal state properties :)
EDIT: I appear to have misunderstood the question...
I am trying to learn ReactJS and Redux, and have come across a problem that I cannot seem to get over.
I have a React component, that gets data from an asynchronous request.
export class MyPage extends React.Component {
constructor(props) {
super(props)
this.state = {
enableFeature: false,
}
this.handleEnableFeatureChange = this.handleEnableFeatureChange.bind(this)
}
componentWillMount () {
this.fetchData()
}
fetchData () {
let token = this.props.token
this.props.actions.fetchData(token)
}
handleEnableFeatureChange (event) {
this.setState({ enableFeature: event.target.checked })
}
render () {
if (this.props.isFetching) {
return (
<div>Loading...</div>
)
} else {
return (
<div>
<label>Enable Feature
<input type="checkbox"
className="form-control"
checked={this.props.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
</div>
)
}
}
}
So, my problem now is that, when I change the state of the checkbox, I want to update the state of my data. However, every time I update the state of my data, the react component method shouldComponentUpdate kicks in, and uses the current props to render the original data.
I would like to see how such cases are handled in general.
Thanks.
Try to do it like the following, i.e.
Use componentWillReceiveProps to assign props.enableFeature to state.enableFeature. From documentation
componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render.
componentWillReceiveProps() is not invoked if you just call this.setState()
Use this state to load the value of checkbox
Manipulate this state (onchange) to update the value of checkbox
Following code can work in your case
export class MyPage extends React.Component {
static propTypes = {
isFetching: React.PropTypes.bool,
enableFeature: React.PropTypes.bool,
token: React.PropTypes.string,
actions: React.PropTypes.shape({
fetchData: React.PropTypes.func
})
};
state = {
enableFeature: false,
};
componentWillMount () {
this.fetchData();
}
/* Assign received prop to state, so that this state can be used in render */
componentWillReceiveProps(nextProps) {
if (this.props.isFetching && !nextProps.isFetching) {
this.state.enableFeature = nextProps.enableFeature;
}
}
fetchData () {
const { token } = this.props;
this.props.actions.fetchData(token)
}
handleEnableFeatureChange = (event) => {
this.setState({ enableFeature: event.target.checked })
};
render () {
return (<div>
{ this.props.isFetching && "Loading..." }
{
!this.props.isFetching && <label>
Enable Feature
<input
type="checkbox"
className="form-control"
checked={this.state.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
}
</div>);
}
}
Note: The above code was not executed, but should work (babel's stage-0 code)
Change it to checked={this.state.enableFeature}