React Respond to Property Object Change - reactjs

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,
}

Related

ReactJS Change Sibling State via Parent

My React structure is
- App
|--SelectStudy
|--ParticipantsTable
In SelectStudy there is a button whose click triggers a message to its sibling, ParticipantsTable, via the App parent. The first Child->Parent transfer works. But how do I implement the second Parent->Child transfer? See questions in comments.
App
class App extends Component {
myCallback(dataFromChild) {
// This callback receives changes from SelectStudy Child Component's button click
// THIS WORKS
alert('SelectStudy Component sent value to Parent (App): ' + dataFromChild.label + " -> " + dataFromChild.value);
// QUESTION: How to Update State of ParticipantsTable (SelectStudy's Sibling) next?
// ........................................................
}
render() {
return (
<div className="App">
<SelectStudy callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable></ParticipantsTable>
</div>
);
}
SelectStudy
class SelectStudy extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.state;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
handleButtonClick = () => {
this.props.callbackFromParent(this.state.selectedStudy);
}
}
ParticipantsTable - this needs to receive a certain variable, e.g. study in its State
class ParticipantsTable extends React.Component {
constructor(props) {
//alert('Constructor');
super(props);
// Initial Definition of this component's state
this.state = {
study: null,
items: [],
error: null
};
}
// THIS METHOD IS AVAILABLE, BUT HOW TO CALL IT FROM App's myCallback(dataFromChild)?
setStudy = (selectedStudy) => {
this.setState({study: selectedStudy});
}
render() {
return ( <div>{this.state.study}</div> );
}
}
The state should live definitively at the App level, not in the child. State needs to live one level above the lowest common denominator that needs access to it. So if both SelectStudy and ParticipantsTable need access to the same bit of state data, then it must live in their closest common ancestor (or above).
This is a core concept of React, known as "lifting state up", so much so that it has its own page in the official React documentation.
In your case, it would look something like this. Notice how state lives in only one place, at the <App /> level, and is passed to children via props.
import React from 'react';
class App extends React.Component {
// State lives here at the closest common ancestor of children that need it
state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
myCallback = (dataFromChild) => {
this.setState(dataFromChild);
};
render() {
return (
<div className="App">
{/* State is passed into child components here, as props */}
<SelectStudy data={this.state} callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable study={this.state.selectedStudy} />
</div>
);
}
}
class SelectStudy extends React.Component {
handleButtonClick = () => {
// Here we execute a callback, provided by <App />, to update state one level up
this.props.callbackFromParent({ ...this.props.selectedStudy, isButtonLoading: true });
};
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.props.data;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
}
// This component doesn't need to track any internal state - it only renders what is given via props
class ParticipantsTable extends React.Component {
render() {
return <div>{this.props.study}</div>;
}
}
I think what you need to understand is the difference between state and props.
state is internal to a component while props are passed down from parents to children
Here is a in-depth answer
So you want to set a state in the parent that you can pass as props to children
1 set state in the parent
this.state = {
value: null
}
myCallback(dataFromChild) {
this.setState({value: dataFromChild.value})
}
2 pass it as a prop to the children
class ParticipantsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
study: props.study,
items: [],
error: null
};
}
Also, although not related to your question, if you learning React I suggest moving away from class-based components in favour of hooks and functional components as they have become more widely used and popular recently.

How to modify a prop and not reflect the change in parent

I want to change some inner property of a prop. Props claim to be immutable, but when I change the value, the parent's state value is getting changed.I read that props are immutable. But changing the value is reflecting in parent.
class ParentComp extends React.Component {
constructor(props) {
super(props);
let property = {someProperty:'ABCD'};
this.state={
myState: property
}
}
render() {
return(
<div>
Parent:{JSON.stringify(this.state.myState)}
<ChildComp pState={this.state.myState} />
</div>
);
}
}
class ChildComp extends React.Component {
render() {
this.props.pState.someProperty = '1234';
return(
<div>
Child:{JSON.stringify(this.props.pState)}
</div>
);
}
}
At the end, I see both the values are changed to 1234. In online fiddles, it is working as expected(ie., parent value is not changed.). But in my project, the parent is being changed.
How do I achieve this usecase, wherein I want to change some properties in props, and not reflect in the parent's state?
I read that props are immutable
That's not true - you only should treat them as they were immutable. In other words, they are just regular javascript Objects and you should not mutate them.
If you want to change the value of the props, you should dump the props in the state first then do the mutation on that state.
Mutating props is not best practice. So your ChildComp should be like:
class ChildComp extends React.Component {
constructor(props) {
super(props);
this.state = {
pState: props.pState,
}
}
componentDidMount() {
const { pState } = this.state.
pState.someProperty = '1234';
this.setState({
pState,
});
}
render() {
return(
<div>
Child:{JSON.stringify(this.state.pState)}
</div>
);
}
}

One React Component. Two HOCs. How to make setState correctly update props?

Given a standard compose function and a 'div' Component, how would you write the two HOCs such that:
The 'div' element starts as a 20px green box, then on click, becomes a 50px blue box.
The concerns of - a: merging state with props, and b: triggering a state change, are handled by separate HOCs.
the updater HOC maps state to props, and sets a default state
the dispatcher HOC accepts a function to get the new state on click
The example below works to get a green box, and correctly fires the handler. The update only happens in the state of the Dispatcher HOC's state. The updater HOC's state remains unchanged, as do its props.
I'm really curious to understand what's happening. Flipping the two HOCs' order in compose causes the handler not to be set. Since they both merge in {...this.props}, that doesn't make sense to me. Guessing there's something I don't understand about how multiple HOCs merge props and state.
const HOCDispatcher = myFunc => BaseComponent => {
return class Dispatcher extends React.Component {
constructor(props,context){
super(props,context);
this.handlerFn = (event)=>{this.setState(myFunc)}
}
render(){
return createElement(BaseComponent,{...this.props,onClick:this.handlerFn});
}
}
}
const HOCUpdater = defaultState => BaseComponent => {
return class Updater extends React.Component {
constructor(props,context){
super(props,context);
this.state = Object.assign({},defaultState,this.state);
}
render(){
return createElement(BaseComponent,{...this.props,...this.state});
}
}
}
const MyComponent = compose(
HOCDispatcher(()=>({
style:{width:'50px',height:'50px',background:'blue'}
})),
HOCUpdater({
style:{width:'20px',height:'20px',background:'green'}
}),
)('div');
If you try to simplify or compile your code in a way to a less complicated structure you can understand it better:
The initial version of MyComponent
const MyComponent= class Dispatcher extends React.Component {
constructor(props,context){
super(props,context);
this.handlerFn = (event)=>{this.setState({
style:{width:'50px',height:'50px',background:'blue'}
})}
}
render(){
return <HOCUpdater onClick={this.handlerFn}/>
}
}
Where HOCUpdater also renders as:
class Updater extends React.Component {
constructor(props,context){
super(props,context);
this.state = {
style:{width:'20px',height:'20px',background:'green'}
};
}
render(){
return <div style:{width:'20px',height:'20px',background:'green'}/>;
}
}
Thus rendering the green box.
After triggering the click
const MyComponent= class Dispatcher extends React.Component {
constructor(props,context){
super(props,context);
this.handlerFn = (event)=>{this.setState({
style:{width:'50px',height:'50px',background:'blue'}
})};
this.state= {
style:{width:'50px',height:'50px',background:'blue'}
};
}
render(){
return <HOCUpdater onClick={this.handlerFn}/>
}
}
If you pay attention to the render, it's still the same because this.props has not changed and it is still empty. Thus no change to the style of the box whereas the state of the Dispatcher is changed!
Did you see where you went wrong? Well, just change this.props to this.state in the Dispatcher and you'll see the magic happen.
But wait, there's more!
What happens if you have a line of code like this?
createElement('div',{
style:{width:'50px',height:'50px',background:'blue'},
style:{width:'20px',height:'20px',background:'green'}
});
Well, it still renders the first one (the blue box) but to avoid this try changing the render method of HOCUpdater to this:
return createElement(BaseComponent,{...this.state});
and also add a componentWillReceiveProps method, so your HOCUpdater will look like this:
const HOCUpdater = defaultState => BaseComponent => {
return class Updater extends React.Component {
constructor(props,context){
super(props,context);
this.state = Object.assign({},defaultState,this.state);
}
componentWillReceiveProps(nextProps){
this.setState(nextProps);
}
render(){
return createElement(BaseComponent,{...this.state});
}
}
}

How to make simple mobx reaction work in sub component

I miss something in mobx observables and reactions.
I prepared two examples, one of them works, the other does not, I don't understand why.
Example 1 (does not work):
#observer class Alert1 extends Component {
constructor(props) {
super(props);
this.r2 = reaction(
() => this.props.v1,
v => console.log("Alert1 reaction trigger",v)
);
}
render() {
return null;
}
}
#observer class Main extends Component {
#observable v1 = false;
render() {
return (
<div>
<Alert1 v1={this.v1} />
<button onClick={e=>this.v1=!this.v1}>Switch</button>
</div>
);
}
}
In example 1 we just send observable variable in props and create reaction in Alert1 component, but it does not trigger.
Example 2 (works):
#observer class Alert2 extends Component {
constructor(props) {
super(props);
this.r2 = reaction(
() => this.props.someObj.v1,
v => console.log("Alert2 reaction trigger",v)
);
}
render() {
return null;
}
}
#observer class Main extends Component {
constructor(props) {
this.someObj = observable({v1:observable(false)});
}
render() {
return (
<div>
<Alert2 someObj={this.someObj} />
<button onClick={e=>this.someObj.v1=!this.someObj.v1}>Switch</button>
</div>
);
}
}
That's almost the same as example 1, but we wrap v1 observable into the other observable. Alert2 reaction works.
The same time if we move reactions from Alert1 and Alert2 components to the Main component's constructor, both reactions works.
Here's jsfiddle example with both components, https://jsfiddle.net/kasheftin/zex0qjvf/1/
See https://mobxjs.github.io/mobx/best/react.html, in your first example, you are not passing an observable around, but just a plain boolean value (true or false), so there is nothing for the reaction to react to. In javascript, all values are immutable so per definition something that is observable. It are the properties that are observable.
In the second example you pass an object with an observable property, so that is something that can be reacted to.
Note that creating a boxed observable would also work, as those can be passed around as first class citizens. E.g.: v1 = observable(false) and reaction(() => this.props.v1.get(), ...

componentDidMount() not being called when react component is mounted

I've been attempting to fetch some data from a server and for some odd reason componentDidMount() is not firing as it should be. I added a console.log() statement inside of componentDidMount() to check if it was firing. I know the request to the server works as it should As I used it outside of react and it worked as it should.
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
obj: {}
};
};
getAllStarShips () {
reachGraphQL('http://localhost:4000/', `{
allStarships(first: 7) {
edges {
node {
id
name
model
costInCredits
pilotConnection {
edges {
node {
...pilotFragment
}
}
}
}
}
}
}
fragment pilotFragment on Person {
name
homeworld { name }
}`, {}). then((data) => {
console.log('getALL:', JSON.stringify(data, null, 2))
this.setState({
obj: data
});
});
}
componentDidMount() {
console.log('Check to see if firing')
this.getAllStarShips();
}
render() {
console.log('state:',JSON.stringify(this.state.obj, null, 2));
return (
<div>
<h1>React-Reach!</h1>
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
</div>
);
}
}
render(
<App></App>,
document.getElementById('app')
);
The issue here is that the render method is crashing, because the following line is generating an error
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
Fix this to not use this.state.obj.allStarships.edges[1].node.name directly, unless you can guarantee that each receiver is defined.
Check your component's key
Another thing that will cause this to happen is if your component does not have a key. In React, the key property is used to determine whether a change is just new properties for a component or if the change is a new component.
React will only unmount the old component and mount a new one if the key changed. If you're seeing cases where componentDidMount() is not being called, make sure your component has a unique key.
With the key set, React will interpret them as different components and handle unmounting and mounting.
Example Without a Key:
<SomeComponent prop1={foo} />
Example with a Key
const key = foo.getUniqueId()
<SomeComponent key={key} prop1={foo} />
Also check that you don't have more than one componentDidMount if you have a component with a lot of code. It's a good idea to keep lifecycle methods near the top after the constructor.
I encountered this issue (componentDidMount() not being called) because my component was adding an attribute to the component state in the constructor, but not in the Component declaration. It caused a runtime failure.
Problem:
class Abc extends React.Component<props, {}> {
this.state = { newAttr: false }; ...
Fix:
class Abc extends React.Component<props, {newAttr: boolean}> {
this.state = { newAttr: false }; ...

Resources