نگاهی عمیق‌تر به useEffect در ری‌اکت

Take a Deeper Look at useEffect in React

23 بهمن 1399
نگاهی عمیق تر به useEffect

استفاده از useEffect در ری اکت در وهله اول کمی گیج کننده است چرا که componentDidUpdate و componentDidMount را با هم ترکیب کرده و هر دفعه اجرا می شود. از طرفی همانطور که گفتم می توانیم درون آن درخواست های HTTP ارسال کنیم:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
    })

اگر واقعا یک درخواست HTTP در آن داشته باشیم چطور؟ این درخواست در هربار ارسال خواهد شد! بنابراین اگر بخواهیم درخواست HTTP فرضی ما فقط در دفعه اول ارسال شود باید چه کار کنیم؟

برای اینکه این مشکل را به خوبی درک کنید از تابع setTimeout استفاده میکنم تا حالت درخواست HTTP را شبیه سازی کنیم:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);
    })

این تابع پس از یک ثانیه پیام درون alert را نمایش می دهد. فرض کنید این کد ساده یک درخواست HTTP است که برای سروری ارسال می شود و سپس پیامی به ما می دهد (اینجا برای مثال گفته ایم که داده ها در فضای ابری ذخیره شد). ذخیره داده ها در فضای ابری در دفعه اول مشکلی ندارد. اگر به مرورگر بروید پس از 1 ثانیه پیام alert را می گیرید و سپس اگر روی دکمه Toggle Persons کلیک کنید دوباره پس از 1 ثانیه پیام alert را می گیرید و اگر input را تغییر دهید دوباره پیام را دریافت می کنید و الی آخر... . متوجه مشکل شدید؟

ما باید به آن بگوییم فقط زمانی داده ها را در فضای ابری ذخیره کن که Persons تغییر پیدا کند، نه در حالت های دیگر. برای این کار می توانید یک آرگومان دیگر به useEffect بدهید؛ این آرگومان یک آرایه است که باید در آن به داده هایی اشاره کنید که درون useEffect از آنها استفاده شده است. به طور مثال اگر بگوییم که «فقط در حالت تغییر persons تغییر کن» می توان کد را بدین شکل نوشت:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);
    }, [props.persons]);

نکته: اگر effect های مختلفی داشته باشید که با داده های مختلفی کار می کنند، می توانید چندبار از useEffect درون همین تابع استفاده کنید. مشکلی در این زمینه وجود ندارد.

اگر الان به مرورگر بروید با کلیک روی دکمه Toggle Persons هیچ پیامی نمایش داده نمی شود. فقط زمانی که persons را تغییر دهید (تایپ در input، کلیک روی p برای حذف person و...) پیام را مشاهده خواهید کرد.

حالا اگر بخواهیم این پیام فقط در دفعه اول نمایش داده شود چطور؟ ساده است، باید یک آرایه خالی را به آن پاس دهید!

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);
    }, []);

حالا اگر به مرورگر بروید پیام را فقط یک بار (هنگام بارگذاری صفحه) مشاهده می کنید. بنابراین با پاس دادن یک آرایه خالی به useEffect می توانید componentDidMount را شبیه سازی کنید!

پاک سازی کدهای اضافی

می دانیم که کامپوننت Persons با کلیک روی دکمه Toggle Person از DOM حذف می شود. حالا اگر بخواهیم کدهای باقی مانده از آن (مانند یک event-listener یا هر چیز دیگری) را پاک سازی کنیم چطور؟ در پروژه کوچک ما نیازی به پاک سازی نیست اما در پروژه های بزرگ، پاک سازی از اصول مهم است.

برای این کار می توان از componentWillUnmount استفاده کنید، بنابراین آن را درون فایل Persons.js (بعد از componentDidUpdate) اضافه می کنیم:

    componentWillUnmount() {
        console.log('[Person.js] componentWillUnmount');
    }

حالا اگر به مرورگر بروید و روی دکمه Toggle Person کلیک کنید persons نمایش داده می شوند اما اگر دوباره روی این دکمه کلیک کنید تا بسته شوند در قسمت console پیام Person.js] componentWillUnmount] را دریافت خواهید کرد:

useEffect در ری اکت
پیام componentWillUnmount در کنسول مرورگر

بنابراین می توانید درون این متد کدهای پاک سازی را بنویسید.

اگر مانند فایل Cockpit.js از hook ها استفاده می کنید، می توانید از همان useEffect استفاده کنید؛ درون تابعی که به عنوان آرگومان به useEffect داده ایم، می توانیم یک تابع دیگر را return کنیم که قبل از تابع اصلی (که به useEffect پاس داده ایم) اما بعد از هر چرخه render اجرا می شود:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

        return () => {
            console.log('[Cockpit.js] cleanup work in useEffect');
        }

    }, []);

اگر در حال حاضر به مرورگر بروید پیام بالا را مشاهده نخواهید کرد، چرا که کامپوننت cockpit هیچ گاه در برنامه ما حذف نمی شود، بنابراین باید آن را حذف کنیم.

در فایل App.js یک دکمه جدید ایجاد می کنیم:

    return (
      <div className={classes.App}>
        <button>Remove Cockpit</button>
        <Cockpit
          title={this.props.appTitle}
          showPersons={this.state.showPersons}
          persons={this.state.persons}
          clicked={this.togglePersonsHandler} />
        {persons}
      </div>
    );

حالا به قسمت State در همین App.js می رویم:

  state = {
    persons: [
      { id: 'asfa1', name: 'Max', age: 28 },
      { id: 'vasdf1', name: 'Manu', age: 29 },
      { id: 'asdf11', name: 'Stephanie', age: 26 }
    ],
    otherState: 'some other value',
    showPersons: false,
    showCockpit: true
  }

می بینید که state دیگری به نام showCockpit را اضافه کرده ایم و در حالت پیش فرض به آن مقدار true داده ایم.

حالا به قسمت JSX برمی گردیم و به در همانجا کد تغییر state را انجام می دهیم:

    return (
      <div className={classes.App}>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        <Cockpit
          title={this.props.appTitle}
          showPersons={this.state.showPersons}
          persons={this.state.persons}
          clicked={this.togglePersonsHandler} />
        {persons}
      </div>
    );

نکته: تغییر State درون خود دکمه روش خوبی نیست و کد شما را شلوغ می کند. ما می خواهیم سریعا این کار را انجام بدهیم بنابراین فعلا مشکلی ندارد.

حالا قسمت شرطی را اضافه می کنیم:

    return (
      <div className={classes.App}>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ?
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            persons={this.state.persons}
            clicked={this.togglePersonsHandler} /> : null}
        {persons}
      </div>
    );

حالا اگر به مرورگر برویم و دکمه remove Cockpit را کلیک کنیم در قسمت console مرورگر پیام cleanup خود را می بینیم:

useEffect در ری اکت
پاکسازی کدها در useEffect

بنابراین اگر به useEffect یک آرایه خالی بدهید:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

        return () => {
            console.log('[Cockpit.js] cleanup work in useEffect');
        }

    }, []);

تابع اصلی درون آن زمانی اجرا می شود که کامپوننت render و unmount شده باشد.

حالا یک useEffect دیگر (در فایل Cockpit.js) پایین تر از همین useEffect ایجاد می کنیم:

    useEffect(() => {
        console.log('[Cockpit.js] 2nd useEffect');
        return () => {
            console.log('[Cockpit.js] cleanup work in 2nd useEffect');
        }
    });

توجه کنید که به این useEffect آرایه خالی (آرگومان دوم) نداده ایم. حالا اگر به مرورگر بروید و روی دکمه Toggle Persons کلیک کنید، در قسمت Console چنین چیزی را می بینید:

useEffect در ری اکت
اجرای پاکسازی قبل از اجرای تابع اصلی

بنابراین پاک سازی قبل از خود useEffect اجرا شده است. این مورد هم می تواند به درد ما بخورد؛ به طور مثال زمانی که عملیاتی داشته باشیم و بخواهیم با re-render شدن یک کامپوننت متوقف شود، می توانیم از این حالت استفاده کنیم.

اگر هنوز بسیاری از این موارد برای شما جا نیفتاده است، نگران نباشید. ما هنوز از هیچ کدام به صورت عملی استفاده نکرده ایم اما بعدا درون پروژه ای که خواهیم داشت با تمام آن ها به صورت عملی آشنا خواهید شد.

اگر الان به مرورگر بروید و صفحه را refresh کنید اما قبل از نمایش پیام alert (درون تابع setTimeout) دکمه Remove Cockpit را کلیک کنیم، باز هم پیام alert نمایش داده می شود. همین مسئله می تواند یک پاک سازی باشد! به طور مثال کد را بدین شکل می نویسیم:

ابتدا تایمر خود را درون یک ثابت قرار می دهیم:

        const timer = setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

سپس زمانی که unmount می شود (درون قسمت return) آن را حذف می کنیم:

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        const timer = setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

        return () => {
            clearTimeout(timer);
            console.log('[Cockpit.js] cleanup work in useEffect');
        };

    }, []);

حالا اگر صفحه را refresh کرده و قبل از نمایش پیام alert دکمه remove Cockpit را کلیک کنیم دیگر شاهد نمایش این پیام نخواهیم بود. این کد فقط یک تست بود بنابراین آن را به حالت قبل برگردانید:

const cockpit = (props) => {

    useEffect(() => {
        console.log('[Cockpit.js] useEffect');
        // an http request here
        setTimeout(() => {
            alert('saved data to cloud!');
        }, 1000);

        return () => {
            console.log('[Cockpit.js] cleanup work in useEffect');
        };

    }, []);

به فایل App.js بروید تا نگاهی به shuoldComponentUpdate بیندازیم. در حال حاضر این متد فقط true برمی گرداند که یعنی با هر بار ایجاد تغییرات درون App.js تمام برنامه re-render می شود. همانطور که گفتم DOM واقعی را بروزرسانی نمی کند اما هنوز هم چک می کند که آیا DOM باید بروزرسانی شود یا خیر و این مسئله باعث کاهش سرعت برنامه ما می شود. به نظر شما چطور می توان آن را اصلاح کرد؟

در قسمت بعد به پاسخ این سوال خواهیم رسید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.