React.js Recommended Assigning `props` Directly To State - reactjs

A React.js warning was thrown:
IndexPage: It is not recommended to assign props directly to state because updates to props won't be reflected in state. In most cases, it is better to use props directly.
I had the following code:
class IndexPage extends React.Component<IProps, IState> {
constructor(props) {
super(props)
this.state = props
}
}
But, when I have changed my code to:
class IndexPage extends React.Component<IProps, IState> {
constructor(props) {
super(props)
this.state = {...props}
}
}
The warning's gone.
Can you please explain why?

Because of how JS assignment of object values works, assigning one object to another means that the second variable will "point" to the same instance, or hold a reference to it:
let x = { v: 1 };
let y = x;
x.v = 2;
console.log(y.v) // prints "2"
The warning is there to prevent accidental assumptions about the props being automatically "propagated" to the state. IOW, it doesn't just work like you'd normally expect:
// assume props = { v: 1 };
this.state = props;
// now props changes to { v: 2 };
console.log(this.state.v) // still prints 1, and that's why you get the warning
The warning goes away because by destructuring, you're making it obvious that a new object is being created, and you don't expect the state to have the same value as props when they change.

Related

React Respond to Property Object Change

I'm looking for a way to update a React.Component when the internal state of one of its properties (an object) changes.
Example:
export class ParentClass extends React.Component {
constructor(props) {
super(props);
this.someObject = new SomeObject();
}
...
render() {
return <ChildClass someObject=this.someObject />
}
}
When I call this.someObject.change() in the parent class. How can I signal to the child class that it should re-render since the internal state of someObject just changed?
Normally, if I updated a primitive value, I believe this would trigger a re-render of the child. However, since the object pointer hasn't changed, I don't believe the child class is aware a change has occurred.
Is it possible to trigger a re-render of ChildClass in this case? Is there maybe a better convention for managing this relationship?
This could have a performance impact, but I resolved this by moving someObject into the state of the parent and setting the Child property to {this.state.someObject}.
When someObject (in this case roadTileMatrix) changes, I clone the object, make the changes, then set the new object as the state. This seems to be a common pattern from my reading.
Alternatively, as a more lightweight solution, it looks like you could instead set a key value in the state that will change each time changes are made to someObject, triggering a re-render of the child.
Code here:
import React from 'react'
import PropTypes from 'prop-types';
import { RoadTileMatrix, Grid } from './Grid';
import { TravelGraph } from './TravelGraph';
export class RoadNetwork extends React.Component {
constructor(props) {
super(props);
const rows = this.props.rows;
const cols = this.props.cols;
this.state = {
roadTileMatrix: new RoadTileMatrix(rows, cols, null)
};
this.addTile = this.addTile.bind(this);
}
addTile(r, c) {
const rows = this.props.rows;
const cols = this.props.cols;
const oldArray = this.state.roadTileMatrix.innerArray;
const newRoadTileMatrix = new RoadTileMatrix(rows, cols, oldArray);
newRoadTileMatrix.addTile(r, c, true);
this.setState({
roadTileMatrix: newRoadTileMatrix,
});
}
render() {
return (
<div>
<Grid roadTileMatrix={this.state.roadTileMatrix} addTile={this.addTile} />
<TravelGraph />
</div>
);
};
}
RoadNetwork.propTypes = {
rows: PropTypes.number.isRequired,
cols: PropTypes.number.isRequired,
}

Modify states from a non react.component class

In my file I have a Main class which extends React.Component. The class is exported at the end of the file. It has some states, one of which is a list (Object[]).
Within the file there is another class (let's call it Foo) which has other functions. I create a reference to Foo within Main, and then use that to call bar() within Foo.
In bar, I have a for loop which should add 5 TestComponents to that list mentioned above.
Since it's a state, I also have Foo extending React.Component. The issue is that even though that allows me to use setState(), the component is never mounted so when you modify the state you get:
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the Foo component.
If I remove the extends React.Component from Foo then of course, I can't change the state since I can't use setState.
Here is a very simplified version of my main code:
interface State {
list: Object[];
}
class Main extends React.Component<{}, State> {
foo = new Foo();
state = {
list: [],
}
componentDidMount() {
console.log(this.state.list); //logs: []
this.foo.bar();
console.log(this.state.list); //logs: [] expected: [TestComponent....]
}
render() {
return (
<IonContent>
<IonList>
{
//this.list.map(...);
}
</IonList>
</IonContent>
);
};
};
class Foo extends React.Component<{}, State> {
state = {
list: [],
}
constructor(props) {
super(props);
}
componentDidMount() {
console.log("mounted"); //doesnt log this since component doesnt get mounted
}
bar() {
for(let i = 0; i < 5; i++) {
let a = <TestComponent key={i}></TestComponent>
this.setState({
list: [...this.state.list, a], //errors here saying component not mounted
});
}
}
}
export default Main;
How can I modify the list state from within Foo since Foo is never mounted as a component ('mounted' is never logged)?
I tried seeing if a render function would mount it but like I thought it didn't.
render() {
return(
<></>
);
}
I got this to work using globals (this should solve two problems at once. I needed the array 'global' within a single script but also within every script)
I did this by creating a globals.tsx file.
Within the file I added:
export var bluetoothDevices: Object[] = [];
then to access it the global you can do:
import { bluetoothDevices } from '/globals/globals'
bluetoothDevices.push(x);
and it's as simple as just pushing anything to the array. I can confirm this works within the file (I can modify and read it from each class) although I can't confirm if it works throughout all scripts since I only have 1 page at the moment.

Where should a state be defined?

What is the difference between these two constructs of defining state in React?
class ProductsPage extends Component {
constructor(props) {
super(props);
this.state = {
products: []
};
}
...
}
and this:
class ProductsPage extends Component {
state = {
products: []
};
...
}
Both of them work well when coded in ES6. However, the lower one doesn't seem to work in typescript 3. I have the following:
interface IState {
products: IProduct[];
}
class ProductsPage extends Component<{}, IState> {
state = {
products: []
};
public componentDidMount() {
this.setState({ products });
}
public render() {
return (
<div className="page-container">
<ul className="product-list">
{this.state.products.map(p => (
<li className="product-list-item" key={p.id}>
{p.name}
</li>
))}
</ul>
</div>
);
}
}
and the ts compiler flagged an error saying: Property id does not exist on type 'never'
Why is that?
The second form is class properties which is a stage 3 JavaScript proposal (meaning it's not part of the language yet). Adding properties on the constructor is the old ES2015 way (called maximally minimal classes).
In your case there is no functional difference.
TypeScript requires you to declare class fields for type safety - hence the warning.
How to define the state for a React component is as subjective as the coding styles React promotes itself. Usually I go the very strict route which looks as follows:
type State = {
someStateVar: number[];
};
export class MyComponent extends Component<{}, State> {
public readonly state: State = {
someStateVar: [],
};
public async componentDidMount() {
// Dynamically fetch data (maybe via axios) and populate the new state
const data = await axios.get<number[]>(...);
this.setState({ someStateVar: data });
}
}
As you can see, I explicitly mark state as readonly just make sure, nobody attempts to write directly to it (even though IDEs and linters can check for those errors without the precaution nowadays).
Another reason why I prefer to not set the state manually with an assigment is that it might encourage wrong handling of state. You are never to assign something directly to state in a class method without the use of setState.
Furthermore I am basically defining the defaults right away and populating the state with dynamic data in componentDidMount. This approach allows you to keep the code concise by dropping an explicit constructor definition as you should move such an initialization to componentDidMount anyways and use the constructor only for binding methods if you don't use the class member arrow notation for those in the first place.

React add unmounted component to the array

Im trying to create multiple components for future rendering adding tham to the array like this:
widgets.push(<TextWidget fieldData={fieldData} correctionFactor={correctionFactor} />);
but in my component I'm getting
TypeError: Cannot set property 'FieldData' of undefined
class TextWidget extends Component {
FieldData = null;
CorrectionFactor = null;
state = {
FieldData: null,
CorrectionFactor: null
}
constructor(props) {
this.FieldData = props.fieldData;
this.CorrectionFactor = props.correctionFactor || 1;
}
componentDidMount() {
this.state.FieldData = this.FieldData;
this.state.CorrectionFactor = this.CorrectionFactor;
}
....
if i do smth like this.state.FieldData = props.FieldData; in a constructor then react is complaining about being unable to set state of unmounted component.
I think that you forgot call super() inside your constructor as a first line
super(props);
According to the React docs:
You should call super(props) before any other statement. Otherwise, this.props will be undefined in the constructor, which can lead to bugs.
You're committing two mistakes.
First: You should call the super(props) before manipulating the props, then use the this.props.FieldData, and you could do it even in the constructor(), when defining the state, like:
constructor(props) {
super(props);
this.state = {
FieldData: this.props.FieldData,
CorrectionFactor: this.props.CorrectionFactor || 1
};
}
Second: You shouldn't set state like you did:
this.state.FieldData = this.FieldData;
You should use the this.setState()(read the docs), like below:
this.setState({
FieldData: this.props.FieldData,
CorrectionFactor: this.props.CorrectionFactor
});

Is Initializing state with props object causes mutation?

In my React application, one of the components needs state initialization from props.
Class ComponentA extends React.Component{
constructor(props){
this.state = {
objectA: props.objectA
}
}
someOnclickFunction(e){
let updatedObjA = this.state.objectA;
updatedObjA.value = e.target.value;
this.setState({
objectA: updatedObjA
})
}
}
In the above code snippet, props.objectA reference is copied to state. So, Am I mutating the props indirectly by updating the state?
Or setState() function will clone the object and keep new reference for the objectA?
class ComponentA extends React.Component {
constructor(props) {
super(props);
// state is null at this point, so you can't do the below.
// this.state.objectA = props.objectA
// instead, initialize the state like this:
this.state = {
objectA: props.objectA,
};
}
someOnclickFunction(e) {
// you can't set "objectA.value" like the following
// this.setState({
// objectA.value: e.target.value
// });
// you need to create a new object with your property changed, like this:
this.setState({
objectA: Object.assign({}, this.state.objectA, { value: e.target.value }),
})
}
}
This is a mistake that many beginners at react make. You can't simply update sub-properties of an object without consequences.. The following would also be wrong:
someOnclickFunction(e) {
var tmp = this.state.objectA;
// WRONG: don't do this either, you're modifying the state by doing this.
tmp.value = e.target.value;
this.setState({
objectA: tmp,
});
}
To elaborate on the correct way to do this using Object.assign.. this function takes all the parameters and merges them into the first element. So by providing a new object as the first parameter, you've created a copy of your first object with your new property.
Object.assign({}) // = {}
Object.assign({}, objectA) // = copy of objectA
Object.assign({}, objectA, { value: "newValue" }) // = copy of objectA with 'value' = 'newValue'.
Note: Object.assign() is a shallow clone, not a deep clone.

Resources