ตอบสนอง - setState () บนส่วนประกอบที่ไม่ได้ต่อเชื่อม


92

ในส่วนประกอบการตอบสนองของฉันฉันพยายามใช้สปินเนอร์แบบธรรมดาในขณะที่คำขอ ajax กำลังดำเนินการอยู่ - ฉันใช้สถานะเพื่อเก็บสถานะการโหลด

ด้วยเหตุผลบางประการโค้ดด้านล่างในองค์ประกอบการตอบสนองของฉันทำให้เกิดข้อผิดพลาดนี้

อัปเดตได้เฉพาะส่วนประกอบที่ติดตั้งหรือติดตั้งเท่านั้น โดยปกติจะหมายถึงคุณเรียกว่า setState () บนส่วนประกอบที่ไม่ได้ต่อเชื่อม นี่คือ no-op โปรดตรวจสอบรหัสสำหรับส่วนประกอบที่ไม่ได้กำหนด

หากฉันกำจัดการเรียก setState แรกข้อผิดพลาดจะหายไป

constructor(props) {
  super(props);
  this.loadSearches = this.loadSearches.bind(this);

  this.state = {
    loading: false
  }
}

loadSearches() {

  this.setState({
    loading: true,
    searches: []
  });

  console.log('Loading Searches..');

  $.ajax({
    url: this.props.source + '?projectId=' + this.props.projectId,
    dataType: 'json',
    crossDomain: true,
    success: function(data) {
      this.setState({
        loading: false
      });
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
      this.setState({
        loading: false
      });
    }.bind(this)
  });
}

componentDidMount() {
  setInterval(this.loadSearches, this.props.pollInterval);
}

render() {

    let searches = this.state.searches || [];


    return (<div>
          <Table striped bordered condensed hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Submit Date</th>
              <th>Dataset &amp; Datatype</th>
              <th>Results</th>
              <th>Last Downloaded</th>
            </tr>
          </thead>
          {
          searches.map(function(search) {

                let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
                let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
                let records = 0;
                let status = search.status ? search.status.toLowerCase() : ''

                return (
                <tbody key={search.id}>
                  <tr>
                    <td>{search.name}</td>
                    <td>{createdDate}</td>
                    <td>{search.dataset}</td>
                    <td>{records}</td>
                    <td>{downloadedDate}</td>
                  </tr>
                </tbody>
              );
          }
          </Table >
          </div>
      );
  }

คำถามคือเหตุใดฉันจึงได้รับข้อผิดพลาดนี้เมื่อส่วนประกอบควรถูกเมาท์อยู่แล้ว (เนื่องจากถูกเรียกจาก componentDidMount) ฉันคิดว่าการตั้งค่าสถานะเมื่อต่อคอมโพเนนต์นั้นปลอดภัยแล้วหรือไม่


ในตัวสร้างของฉันฉันกำลังตั้งค่า "this.loadSearches = this.loadSearches.bind (this);" - ไม่ดีเพิ่มคำถามนั้น
Marty

คุณได้ลองตั้งค่าการโหลดเป็นโมฆะในตัวสร้างของคุณหรือไม่ นั่นอาจได้ผล this.state = { loading : null };
Pramesh Bajracharya

คำตอบ:


69

การไม่เห็นฟังก์ชันการแสดงผลนั้นค่อนข้างยาก แม้ว่าจะสามารถมองเห็นสิ่งที่คุณควรทำอยู่แล้ว แต่ทุกครั้งที่คุณใช้ช่วงเวลาหนึ่งคุณจะต้องล้างมันเมื่อเลิกต่อเชื่อม ดังนั้น:

componentDidMount() {
    this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}

componentWillUnmount () {
    this.loadInterval && clearInterval(this.loadInterval);
    this.loadInterval = false;
}

เนื่องจากความสำเร็จและการเรียกกลับข้อผิดพลาดเหล่านั้นอาจยังคงถูกเรียกหลังจากยกเลิกการต่อเชื่อมคุณสามารถใช้ตัวแปรช่วงเวลาเพื่อตรวจสอบว่ามีการต่อเชื่อมหรือไม่

this.loadInterval && this.setState({
    loading: false
});

หวังว่านี่จะช่วยได้ให้ฟังก์ชันการแสดงผลหากไม่ได้ผล

ไชโย


2
บรูโนคุณไม่สามารถทดสอบการมีอยู่ของบริบท "นี้" ได้หรือไม่ .. ala this && this.setState .....
james emanon

7
หรือเพียงแค่:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowicz หากคุณกำลังยกเลิกการต่อเชื่อมและติดตั้งส่วนประกอบด้วยตัวจับเวลาก็ยังสามารถยิงได้แม้ว่าคุณจะทำการล้างแบบธรรมดาก็ตาม
corlaez

14

คำถามคือเหตุใดฉันจึงได้รับข้อผิดพลาดนี้เมื่อส่วนประกอบควรถูกเมาท์อยู่แล้ว (เนื่องจากถูกเรียกจาก componentDidMount) ฉันคิดว่าการตั้งค่าสถานะเมื่อต่อคอมโพเนนต์นั้นปลอดภัยแล้วหรือไม่

มันเป็นเรื่องที่ไม่ได้componentDidMountเรียกจาก ของคุณcomponentDidMountสร้างฟังก์ชันเรียกกลับที่จะดำเนินการในสแตกของตัวจัดการตัวจับเวลาไม่ใช่ในสแต็componentDidMountก เห็นได้ชัดว่าเมื่อเรียกกลับ ( this.loadSearches) ของคุณได้รับการดำเนินการคอมโพเนนต์ได้ยกเลิกการต่อเชื่อม

ดังนั้นคำตอบที่ยอมรับจะปกป้องคุณ หากคุณใช้ API แบบอะซิงโครนัสอื่น ๆ ที่ไม่อนุญาตให้คุณยกเลิกฟังก์ชันอะซิงโครนัส (ส่งไปยังตัวจัดการบางตัวแล้ว) คุณสามารถทำสิ่งต่อไปนี้ได้:

if (this.isMounted())
     this.setState(...

การดำเนินการนี้จะกำจัดข้อความแสดงข้อผิดพลาดที่คุณรายงานในทุกกรณีแม้ว่าจะรู้สึกเหมือนกำลังกวาดสิ่งของอยู่ใต้พรมโดยเฉพาะอย่างยิ่งหาก API ของคุณมีความสามารถในการยกเลิก (เช่นเดียวsetIntervalกับclearInterval)


13
isMountedเป็นแอนตี้แพตเทิร์นที่ facebook ไม่แนะนำให้ใช้: facebook.github.io/react/blog/2015/12/16/…
Marty

1
ใช่ฉันบอกว่า "มันให้ความรู้สึกเหมือนกำลังกวาดสิ่งของอยู่ใต้พรม"
Marcus Junius Brutus

5

สำหรับผู้ที่ต้องการตัวเลือกอื่นวิธีการเรียกกลับของ ref แอตทริบิวต์อาจเป็นวิธีแก้ปัญหาชั่วคราว พารามิเตอร์ของ handleRef คือการอ้างอิงถึงองค์ประกอบ div DOM

สำหรับข้อมูลโดยละเอียดเกี่ยวกับการอ้างอิงและ DOM: https://facebook.github.io/react/docs/refs-and-the-dom.html

handleRef = (divElement) => {
 if(divElement){
  //set state here
 }
}

render(){
 return (
  <div ref={this.handleRef}>
  </div>
 )
}

5
การใช้ ref สำหรับ "isMounted" อย่างมีประสิทธิภาพนั้นเหมือนกับการใช้ isMounted แต่มีความชัดเจนน้อยกว่า isMounted ไม่ใช่การต่อต้านรูปแบบเนื่องจากชื่อของมัน แต่เป็นรูปแบบต่อต้านที่จะเก็บการอ้างอิงถึงส่วนประกอบที่ไม่ได้ต่อเชื่อม
Pajn

3
class myClass extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      data: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;
    this._getData();
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _getData() {
    axios.get('https://example.com')
      .then(data => {
        if (this._isMounted) {
          this.setState({ data })
        }
      });
  }


  render() {
    ...
  }
}

มีวิธีที่จะบรรลุสิ่งนี้สำหรับส่วนประกอบที่ใช้งานได้หรือไม่? @john_per
Tamjid

สำหรับส่วนประกอบของฟังก์ชันฉันจะใช้ ref: const _isMounted = useRef (false); @Tamjid
john_per

1

สำหรับลูกหลาน

ในกรณีของเราข้อผิดพลาดนี้เกี่ยวข้องกับ Reflux การโทรกลับการเปลี่ยนเส้นทางและ setState เราส่ง setState ไปยังการเรียกกลับ onDone แต่เรายังส่งการเปลี่ยนเส้นทางไปยังการเรียกกลับ onSuccess ในกรณีของความสำเร็จเรียกกลับ onSuccess ของเราก่อนที่จะดำเนินการ onDone นี้ทำให้เกิดการเปลี่ยนเส้นทางก่อน setState ดังนั้นข้อผิดพลาด setState บนส่วนประกอบที่ไม่ได้ต่อเชื่อม

การดำเนินการจัดเก็บกรดไหลย้อน:

generateWorkflow: function(
    workflowTemplate,
    trackingNumber,
    done,
    onSuccess,
    onFail)
{...

โทรก่อนแก้ไข:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    this.setLoading.bind(this, false),
    this.successRedirect
);

โทรหลังจากแก้ไข:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    null,
    this.successRedirect,
    this.setLoading.bind(this, false)
);

มากกว่า

ในบางกรณีเนื่องจาก isMounted ของ React เป็น "เลิกใช้งาน / ต่อต้านรูปแบบ" เราจึงนำการใช้ตัวแปร _mounted มาใช้และตรวจสอบด้วยตัวเอง


1

แบ่งปันวิธีการแก้ปัญหาการใช้งานโดยตอบสนองตะขอ

React.useEffect(() => {
  let isSubscribed = true

  callApi(...)
    .catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
    .then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
    .catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))

  return () => (isSubscribed = false)
}, [])

โซลูชันเดียวกันนี้สามารถขยายได้เมื่อใดก็ตามที่คุณต้องการยกเลิกคำขอก่อนหน้านี้เกี่ยวกับการเปลี่ยนแปลงรหัสการดึงข้อมูลมิฉะนั้นจะมีเงื่อนไขการแข่งขันระหว่างคำขอในเที่ยวบินหลายรายการ ( this.setStateเรียกว่าไม่อยู่ในลำดับ)

React.useEffect(() => {
  let isCancelled = false

  callApi(id).then(...).catch(...) // similar to above

  return () => (isCancelled = true)
}, [id])

ใช้งานได้เนื่องจากการปิดในจาวาสคริปต์

โดยทั่วไปแล้วแนวคิดข้างต้นใกล้เคียงกับแนวทาง makeCancelable ที่แนะนำโดย react doc ซึ่งระบุไว้อย่างชัดเจน

isMounted เป็น Antipattern

เครดิต

https://juliangaramendy.dev/use-promise-subscription/

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.