Umbro
Umbro

Reputation: 2204

getDerivedStateFromProps, change of state under the influence of changing props

I click Item -> I get data from url:https: // app / api / v1 / asset / $ {id}. The data is saved in loadItemId. I am moving loadItemId from the component Items to the component Details, then to the component AnotherItem. Each time I click Item the props loadItemId changes in the getDerivedStateFromProps method. Problem: I'll click Element D -> I see in console.log 'true', then I'll click Element E --> It display in console.log true andfalse simultaneously, and it should display only false.

Trying to create a ternary operator {this.state.itemX ['completed'] ? this.start () : ''}. If {this.state.itemX ['completed'] call the function this.start ()

Code here: stackblitz

Picture: https://i.sstatic.net/MwGY5.jpg

Items

class Items extends Component {
  constructor (props) {
    super(props);
    this.state = {   
      itemId: null,  
      loadItemId: ''
    }
  }

  selectItem = (id) => {
    this.setState({
      itemId: id
    })

        this.load(id);
  }

    load = (id) => {

        axios.get
            axios({
                    url: `https://app/api/v1/asset/${id}`,
                    method: "GET",
                    headers: {
                        'Authorization': `Bearer ${token}`           
                    }
            })
            .then(response => {
                    this.setState({
                            loadItemId: response.data
                    });
            })
            .catch(error => {
                    console.log(error);
            })
}

  render () {
    return (
            <div > 
                <Item
                    key={item.id}
                    item={item}
                    selectItem={this.selectItem}
                >
                <Details
                    loadItemId={this.state.loadTime}  
                /> 
            </div>
    )
  }

Item

class Item extends Component {
  render () {

    return (
      <div  onClick={() => this.props.selectItem(item.id}>

      </div>
    )
  } 
}

Details

class Details extends Component {
    constructor() {
        super();
    }

  render () {
    return (
            <div> 
                <AnotherItem 
                    loadItemId = {this.props.loadItemId}        
                />       
            </div>
    )
  } 
}

AnotherItem

class AnotherItem extends Component {


  constructor() {
    super();

    this.state = {
      itemX: ''
    };
  }


  static getDerivedStateFromProps(nextProps, prevState) {
    if(nextProps.loadItemId !== prevState.loadItemId) {
      return { itemX: nextProps.loadItemId }
    }

  render () {
      console.log(this.state.itemX ? this.state.itemX['completed'] : '');

    {/*if this.state.loadX['completed'] === true, call function this.start()*/ }
    return (
            <button /*{this.state.loadX['completed'] ? this.start() : ''}*/ onClick={this.start}>
                Start
            </button>      
    );
  }
}

Upvotes: 6

Views: 1824

Answers (3)

farbod
farbod

Reputation: 91

here:

selectItem = (id) => {
    this.setState({
        itemId: id
    })

    this.load(id);
}

you call setState(), then 'Item' and 'Details' and 'AnotherItem' call their render method. so you see log for previous 'loadItemId'.

when 'load' method work done. here:

 this.setState({
     loadItemId: response.data
 });

you setState() again, then 'Item' and 'Details' and 'AnotherItem' call their render method again. in this time you see log for new 'loadItemId'.


solution

setState both state in one place. after load method done, instead of:

 this.setState({
     loadItemId: response.data
 });

write:

 this.setState({
     itemId: id,
     loadItemId: response.data
 });

and remove:

this.setState({
  itemId: id
})

from 'selectItem' method.

Upvotes: 1

zhuber
zhuber

Reputation: 5524

You are encountering this behaviour because you are changing state of Items component on each click with

this.setState({
    itemId: id
})

When changing its state, Items component rerenders causing AnotherItem to rerender (because that is child component) with it's previous state which has completed as true (since you've clicked element D before). Then async request completes and another rerender is caused with

this.setState({
    loadItemId: response.data
});

which initiates another AnotherItem rerender and expected result which is false.

Try removing state change in selectItem and you'll get desired result.

I'd suggest you read this article and try to structure your code differently.

EDIT You can easily fix this with adding loader to your component:

selectItem = (id) => {
    this.setState({
        itemId: id,
        loading: true
    })

    this.load(id);
}

load = (id) => {
    axios.get
        axios({
            url: `https://jsonplaceholder.typicode.com/todos/${id}`,
            method: "GET"
        })
        .then(response => {
            this.setState({
                loading: false,
                loadItemId: response.data
            });
        })
        .catch(error => {
            console.log(error);
        })
}


render() {
    return (
        <div >
            <ul>
                {this.state.items.map((item, index) =>
                    <Item
                        key={item.id}
                        item={item}
                        selectItem={this.selectItem}
                    />
                )
                }
            </ul>
            {this.state.loading ? <span>Loading...</span> : <Details
                itemId={this.state.itemId}
                loadItemId={this.state.loadItemId}
            />}
        </div>
    )
}

This way, you'll rerender your Details component only when you have data fetched and no unnecessary rerenders will occur.

Upvotes: 0

Greg Brodzik
Greg Brodzik

Reputation: 1817

Need some clarification, but think I can still address this at high level. As suggested in comment above, with the information presented, it does not seem that your component AnotherItem actually needs to maintain state to determine the correct time at which to invoke start() method (although it may need to be stateful for other reasons, as noted below).

It appears the functionality you are trying to achieve (invoke start method at particular time) can be completed solely with a comparison of old/new props by the componentDidUpdate lifecycle method. As provided by the React docs, getDerivedStateFromProps is actually reserved for a few 'rare' cases, none of which I believe are present here. Rather, it seems that you want to call a certain method, perhaps perform some calculation, when new props are received and meet a certain condition (e.g., not equal to old props). That can be achieved by hooking into componentDidUpdate.

class AnotherItem extends Component {
   constructor(props) {
      super(props);
      this.state  = {}
   }

   start = () => { do something, perform a calculation }

  // Invoked when new props are passed
  componentDidUpdate(prevProps) {

     // Test condition to determine whether to call start() method based on new props,
     // (can add other conditionals limit number of calls to start, e.g., 
     // compare other properties of loadItemId from prevProps and this.props) .
     if (this.props.loadItemId && this.props.loadItemId.completed === true) {

         //Possibly store result from start() in state if needed
         const result = this.start();
      } 

    }
  }

    render () {
       // Render UI, maybe based on updated state/result of start method if 
       // needed
    );
  }
}

Upvotes: 0

Related Questions