نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
اضافه شدن JavaScript initializers   به Blazor 6x

در اینجا می‌توان فایل ویژه‌ای به نام NAME.lib.module.js را به پوشه‌ی wwwroot پروژه اضافه کرد که name آن، همان نام اسمبلی، کتابخانه و در اصل package identifier پروژه‌است؛ با این محتوا:
export function beforeStart(options, extensions) {
    console.log("beforeStart");
}

export function afterStarted(blazor) {
    console.log("afterStarted");
}
قالب این محتوا باید به همین نحو باشد و معرف اجرای کدهایی پیش از و پس از load برنامه است. به این ترتیب می‌توان به این مزایا دست یافت:
- سفارشی سازی نحوه‌ی بارگذاری یک برنامه‌ی Blazor
- اجرای کدهای سفارشی، پیش و پس از بارگذاری برنامه
- امکان تنظیم ویژگی‌های Blazor

یک مثال: بارگذاری یک اسکریپت پس از کامل شدن بارگذاری Blazor
<body>
    ...

    <script src="_framework/blazor.{webassembly|server}.js" 
        autostart="false"></script>
    <script>
      Blazor.start().then(function () {
        var customScript = document.createElement('script');
        customScript.setAttribute('src', 'scripts.js');
        document.head.appendChild(customScript);
      });
    </script>
</body>
نظرات مطالب
Blazor 5x - قسمت 19 - کار با فرم‌ها - بخش 7 - نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server
خلاصه‌ی بحث جاری به روشی دیگر (مدیریت صحیح طول عمر DbContext در برنامه‌های Blazor Server)

یک روش دیگر مدیریت طول عمر Context در برنامه‌های Blazor Server که تمام نکات این بحث را به نحو ساده‌ای پوشش می‌دهد، به صورت زیر است:
الف) حتما بجای AddDbContext از AddDbContextFactory که در مطلب فوق توضیح داده شد، استفاده کنید تا بتوان هرجائیکه نیاز است یک Context جدید را به صورت دستی ایجاد کرد، تا از به اشتراک گذاری آن و مشکلات مرتبط با طول عمر اینگونه Contextها، مصون ماند:
// Do NOT do this anymore
//services.AddDbContext(o => o.UseSQlite("filename.db"));

// But instead do this:
services.AddDbContextFactory(o => o.UseSQlite("filename.db"));
ب) سپس بجای انواع و اقسام روش‌های مدیریت طول عمر Scoped و سپس Dispose کردن‌ها، آن‌ها را در سرویس‌های برنامه به نحو زیر پیاده سازی و ساده کنید:
public class MyService
{
   private readonly IDbContextFactorey _contextFactory;

   public MyService(IDbContextFactory contextFactory)
   {
      _contextFactory = contextFactory;
   }

   // create a new context for each operation / unit of work
   public async Task DoSomethingAsync()
   {
      using (var ctx = _contextFactory.CreateDbContext())
      {
         // this using clause contains your unit of work
      }
   }
}
در این حالت با استفاده از IDbContextFactory تزریقی، هر جائیکه که نیاز هست، یک Context جدید و همچنین Scoped، ایجاد و همانجا هم در پایان کار، تخریب و Dispose می‌شود و نیازی به پیاده سازی سرویس‌های IDisposable ندارد. این روشی است که در مثال‌های خود مایکروسافت هم مشاهده می‌شود. سپس کار کردن با سرویس فوق، نیازی به نکات مرتبط با inherits OwningComponentBase ندارد و کاملا به همراه injectهای عادی و متداول است. همچنین هم مطمئن هستیم، Context ای که در هر متد استفاده می‌شود، کاملا جدید و غیر اشتراکی است و در پایان کار همان متد، حتما Dispose می‌شود.
اشتراک‌ها
انتقال WebAssembly به سرور یا WASI

Bringing WebAssembly to the .NET Mainstream - Steve Sanderson, Microsoft

Many developers still consider WebAssembly to be a leading-edge, niche technology tied to low-level systems programming languages. However, C# and .NET (open-source, cross-platform technologies used by nearly one-third of all professional developers [1]) have run on WebAssembly since 2017. Blazor WebAssembly brought .NET into the browser on open standards, and is now one of the fastest-growing parts of .NET across enterprises, startups, and hobbyists. Next, with WASI we could let you run .NET in even more places, introducing cloud-native tools and techniques to a wider segment of the global developer community. This is a technical talk showing how we bring .NET to WebAssembly. Steve will demonstrate how it runs both interpreted and AOT-compiled, how an IDE debugger can attach, performance tradeoffs, and how a move from Emscripten to WASI SDK lets it run in Wasmtime/Wasmer or higher-level runtimes like wasmCloud. Secondly, you'll hear lessons learned from Blazor as an open-source project - challenges and misconceptions faced bringing WebAssembly beyond early adopters. [1] StackOverflow survey 2021 

انتقال WebAssembly به سرور یا WASI
نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
یک نکته‌ی تکمیلی: RedirectUri در SignInAsync نیاز به encoding دارد.

روشی که در مطلب جاری مطرح شده، در برنامه‌های Blazor SSR هم قابل استفاده‌است و جهت سهولت بازگشت از صفحه‌ی لاگین به صفحه‌ی قبلی، می‌توان از خاصیت RedirectUri آن بدون نیاز به هیچ متد دیگری استفاده کرد:
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, cookieClaims,
            new AuthenticationProperties
            {
                // ...
                RedirectUri = GetSafeRedirectUri(ReturnUrl)
            });
پیاده سازی متد GetSafeRedirectUri را در اینجا می‌توانید مشاهده کنید که هدف از آن، مقاومت در برابر حملات Open redirect است. فقط ... نکته‌ی مهمی که در اینجا لحاظ شده، encoding آن است که بدون آن، به خطای زیر خواهیم رسید:
Unhandled exception rendering component: Invalid non-ASCII or control character in header: 0x062A 
System.InvalidOperationException: Invalid non-ASCII or control character in header: 0x062A at 
void Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowInvalidHeaderCharacter(char ch)
فرض کنید که RedirectUri نهایی، به همراه حروف فارسی است. چون متد HttpContext.SignInAsync از هدرهای HTTP برای انجام هدایت به صفحه‌ای دیگر استفاده می‌کند،‌ مقادیر این هدرها نمی‌توانند یونیکد باشند و حتما باید encode شوند و اگر اینکار انجام نشود، هر از چندگاهی با خطای فوق مواجه خواهیم شد. متد ویژه‌ی UriHelper.Encode، دقیقا جهت encoding آدرس‌های وب، مخصوص قرارگیری در هدرهای HTTP طراحی شده‌است.
مطالب
بهبود کارآیی نمایش لیست‌ها در Blazor با استفاده از دایرکتیو key@
اگر پیشتر با React کار کرده باشید، احتمالا چنین پیام خطایی را دریافت کرده‌اید:


در اینجا React عنوان می‌کند که هر عنصر از لیستی را که در حال نمایش آن هستید، باید به همراه یک key، ارائه دهید. اما ... این key چیست؟
زمانیکه حالت کامپوننتی تغییر می‌کند (شیء یا اشیایی که به عناصر UI متصل هستند، تغییر می‌کنند)، React، درخت جدیدی از اشیایی را که باید رندر شوند، تولید می‌کند. اکنون React باید محاسبه کند که چه عناصری نسبت به درخت فعلی که در حال نمایش است، تغییر کرده‌اند تا فقط آن‌ها را به DOM اصلی اعمال کند؛ تا این تغییرات به کاربر نمایش داده شوند و ... این کار را هم به خوبی انجام می‌دهد. پس مشکل با لیست‌ها چیست که نیاز به key دارند؟
فرض کنید رندر لیستی، خروجی زیر را تولید می‌کند:
<li>Item 1</li>
<li>Item 2</li>
اکنون یک المان را به انتهای این لیست اضافه می‌کنیم:
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
در این حالت React به خوبی تشخیص می‌دهد که المان سومی به لیست اضافه شده‌است و فقط آن‌را رندر می‌کند؛ بجای اینکه کل لیست را مجددا رندر کند. اما اگر نحوه‌ی اضافه شدن المان چهارمی به لیست جدید، به صورت زیر باشد:
<li>Item 0</li> // <= New item
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
یعنی این المان در ابتدای لیست، اضافه شده باشد، اینبار المان اول لیست سه‌تایی قبلی را با المان اول لیست چهارتایی جدید مقایسه می‌کند (مقایسه‌ی بر اساس ایندکس). چون این دو یکی نیستند، کل لیست جدید را مجددا رندر خواهد کرد؛ و در این حالت دیگر نمی‌تواند تشخیص دهد که المان‌هایی در این لیست هستند که با قبل تفاوتی ندارند.
راه‌حل React برای تشخیص منحصربفرد بودن المان‌های یک لیست و یا آرایه، استفاده از خاصیت key است:
<li key={0}>Item 0</li> // <= New item
<li key={1}>Item 1</li>
<li key={2}>Item 2</li>
<li key={3}>Item 3</li>
در اینجا هر آیتم لیست را با یک key منحصربفرد مشخص می‌کنیم. در این حالت React دقیقا می‌تواند محاسبه کند، عنصری که در آرایه‌ی در حال رندر تغییر کرده‌است، کدام است و فقط آن‌را در DOM نهایی به روز رسانی می‌کند؛ و نه اینکه کل لیست را مجددا رندر کند.

این نکات چه ربطی به Blazor دارند؟!
واقعیت این است که Blazor، همان نسخه‌ی مایکروسافتی React است (!) و این خاصیت key، در Blazor نیز تحت عنوان key directive@ وجود دارد و دقیقا مفهوم آن نیز با توضیحاتی که در مورد React داده شد، یکی است.
زمانیکه Blazor صفحه‌ای را رندر می‌کند، ابتدا یک DOM مشخصی را تولید خواهد کد. سپس با تغییر State یک کامپوننت، DOM جدیدی را محاسبه کرده و آن‌را با DOM فعلی مقایسه می‌کند و در نهایت diff تولیدی را به DOM موجود، در جهت نمایش تغییرات، اعمال خواهد کرد.
بنابراین الگوریتم diff باید اضافات، به روز رسانی‌ها و حذف‌های صورت گرفته‌ی در UI را تشخیص داده و فقط قسمت‌های تغییر یافته را جهت به روز رسانی بهینه‌ی UI، به آن اعمال کند. این الگوریتم diff به صورت پیش‌فرض از ایندکس المان‌ها برای مقایسه‌ی آن‌ها استفاده می‌کند. هرچند این روش در بسیاری از حالات، بهینه‌ترین روش است، اما در مورد لیست‌ها خیر؛ که توضیحات آن‌را با مثالی در مورد React، در ابتدای بحث بررسی کردیم. برای مثال اگر شیءای به انتهای لیست اضافه شود، هر المانی را که پس از این ایندکس قرار گرفته باشد، تغییر یافته درنظر گرفته و آن‌را مجددا رندر می‌کند. به همین جهت است که اگر المانی به ابتدای یک لیست اضافه شود، اینبار کل لیست را مجددا رندر می‌کند (چون تمام ایندکس‌های اشیاء موجود در لیست، تغییر کرده‌اند)؛ صرفنظر از اینکه عناصری از این لیست، پیشتر در UI رندر شده‌اند و نیازی به رندر مجدد، ندارند.


یک مثال: بررسی نحوه‌ی رندر لیستی از اشیاء در Blazor

در اینجا کدهای کامل کامپوننتی را مشاهده می‌کنید که یک لیست ساده را در ابتدا رندر کرده و هر بار که بر روی دکمه‌ی افزودن یک شخص کلیک می‌شود، شخص جدیدی را به ابتدای لیست اضافه می‌کند:
@page "/"

<button class="btn btn-primary" @onclick="addPerson">Add Person</button>

<ul class="mt-5">
    @foreach (var person in People)
    {
        <li>@person.Id, @person.Name</li>
    }
</ul>

@code{

    List<Person> People = new()
    {
        new() { Id = 1, Name = "User 1" },
        new() { Id = 2, Name = "User 2" },
    };
    int LastId = 2;

    void addPerson()
    {
        People.Insert(0, new() { Id = ++LastId, Name = $"User {LastId}" });
    }

    class Person
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}
در این حالت اگر به لیست المان‌های نمایش داده شده‌ی در ابزار‌های توسعه دهندگان مرورگر مراجعه کنیم، با هر بار کلیک بر روی دکمه افزودن یک شخص جدید، محتوای li‌های نمایش داده شده، ابتدا به رنگ صورتی در آمده و سپس عادی می‌شوند. این تغییر رنگ، به معنای عناصری هستند که هم اکنون مجددا رندر شده‌اند و در UI نهایی تغییر کرده‌اند:


همانطور که مشاهده می‌کنید، در مثال فوق هر بار کلیک بر روی دکمه‌ی افزودن یک شخص جدید، به همراه رندر تمام المان‌های لیست است (محتوای تمام li‌ها یکبار صورتی شده و بعد به حالت عادی در می‌آیند).


تغییر الگوریتم diff محاسبه‌ی تغییرات UI

الگوریتم پیش‌فرض diff، از ایندکس‌های عناصر برای یافتن تغییرات، استفاده می‌کند. با استفاده از دایرکتیو ویژه‌ی key@ می‌توان ایندکس‌های پیش‌فرض را با مقادیری منحصربفرد، بازنویسی کرد، تا اینبار Blazor دقیقا بداند که کدام آیتم، جدید است و کدام‌ها نیازی به رندر مجدد ندارند:
<ul class="mt-5">
    @foreach (var person in People)
    {
        <li @key="person.Id">@person.Id, @person.Name</li>
    }
</ul>
در اینجا تنها تغییر صورت گرفته، اضافه کردن دایرکتیو key@ به هر li در حال رندر است که اینبار مقدار آن، دیگر ایندکس پیش‌فرض عناصر نبوده، بلکه کلید منحصربفرد آن‌ها است.


اگر به تصویر فوق دقت کنید، اینبار فقط li جدیدی که اضافه شده‌است، ابتدا با رنگ صورتی نمایش داده می‌شود و محتوای داخل سایر li ها، دست نخورده باقی مانده‌است؛ یعنی مجددا رندر نشده‌اند.
نظرات مطالب
فعال سازی قسمت ارسال فایل و تصویر ویرایشگر آنلاین RedActor در ASP.NET MVC
سلام جناب آقای مهندس
در لینکی که در اول پست گذاشته اید ، وقتی دکمه Get Redactor رو میزنیم ، هیچ نسخه free وجود ندارد!
3 تا گزینه buy a licence وجود دارد و یک گزینه trial version
ممنون میشم اگر لینک مستقیم نسخه رایگان رو بگذارید
با سپاس
نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت اول
با سلام
با تشکر از مطلب خوبی که بیان کردید.
امکان تعریف WorkFlow‌های پیچیده و با شرایط و تنظیمات پیچیده نیز وجود دارد؟
به غیر از این کامپوننت ، کامپوننت‌های دیگری هم وجود داره ؟ اگر ممکنه لیستی از این کامپوننت‌ها را قرار بدهید.
همچنین میشه بعد از رسم کامل workFlow ذخیره سازی را انجام بدیم و بعد دوباره به همان شکل لود کنیم ؟
مطالب
React 16x - قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور
در قسمت‌های 22 تا 25 این سری، روش برقراری ارتباط با سرور را در برنامه‌های React، توسط کتابخانه‌ی معروف Axios، بررسی کردیم. در این قسمت می‌خواهیم همان نکات را زمانیکه قرار است از کامپوننت‌های تابعی، به همراه useState hook و useEffect hook استفاده کنیم، مرور نمائیم.


برپایی پیش‌نیازها

در اینجا نیز از همان برنامه‌ای که در قسمت 30، برای بررسی مثال‌های React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانه‌ی Axios را نیز نصب می‌کنید. به همین جهت در ریشه‌ی پروژه‌ی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
برنامه‌ی backend مورد استفاده هم همان برنامه‌ای است که از قسمت 22 شروع به توسعه‌ی آن کردیم و کدهای کامل آن‌را از پیوست‌های انتهای بحث، می‌توانید دریافت کنید. این برنامه که در مسیر شروع شده‌ی با https://localhost:5001/api قرار می‌گیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شده‌است. برای راه اندازی آن، به پوشه‌ی این برنامه، مراجعه کرده و فایل dotnet_run.bat آن‌را اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
در ادامه می‌خواهیم در برنامه‌ی React خود، لیست مطالب برنامه‌ی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمه‌های search و clear نیز به آن اضافه کنیم.


دریافت اطلاعات اولیه از سرور، درون useEffect Hook

پس از نصب پیش‌نیازها و راه اندازی برنامه‌ی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد می‌کنیم:
{
   "apiUrl": "https://localhost:5001/api"
}
سپس فایل جدید src\components\part03\Search.jsx را جهت توسعه‌ی کامپوننت جستجوی این قسمت ایجاد می‌کنیم و ساختار ابتدایی آن‌را با import وابستگی‌های React و مسیر فوق، به صورت یک function که در همان محل قابل export است، ایجاد می‌کنیم، تا فعلا یک React.Fragment را بازگشت دهد:
import React from "react";
import {apiUrl} from "../../config.json";

export default function App() {
  return <></>;
}
بر این اساس، کامپوننت App فایل index.js را به صورت زیر از کامپوننت App فوق، تامین خواهیم کرد:
import App from "./components/part03/Search";
اکنون می‌خواهیم اولین درخواست خود را به سمت backend server ارسال کنیم. برای این منظور در کامپوننت‌های تابعی، از useEffect Hook استفاده می‌شود؛ چون کار با یک API خارجی نیز یک side effect محسوب می‌گردد. بنابراین متد useEffect را import کرده و سپس آن‌را بالای return، فراخوانی می‌کنیم. درون آن نیاز است اطلاعات را از سرور دریافت کنیم و برای اینکار از کتابخانه‌ی axios که آن‌را در قسمت 23 معرفی کردیم، استفاده خواهیم کرد. به همین جهت import آن‌را نیز در این ماژول خواهیم داشت:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  useEffect(() => {
    axios
      .get(apiUrl + "/posts/search?query=")
      .then(response => console.log(response.data));
  });
  return <></>;
}
در اینجا با استفاده از متد get کتابخانه‌ی axios، درخواستی را به آدرس https://localhost:5001/api/posts/search، با یک کوئری استرینگ خالی، ارسال کرده‌ایم تا تمام داده‌ها را بازگشت دهد. روش قدیمی استفاده‌ی از axios را که با استفاده از Promiseها و متد then آن است، در اینجا ملاحظه می‌کنید که خروجی خاصیت data شیء response دریافتی را لاگ کرده‌است:


اکنون می‌خواهیم این اطلاعات دریافتی را در برنامه‌ی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقه‌ای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آن‌را import کرده و سپس آن‌را تعریف و در قسمت then، فراخوانی می‌کنیم:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  const [results, setResults] = useState([]);

  useEffect(() => {
    axios.get(apiUrl + "/posts/search?query=").then(response => {
      console.log(response.data);
      setResults(response.data);
    });
  });
چون اطلاعات بازگشتی به صورت یک آرایه‌است، مقدار اولیه‌ی متد useState را با یک آرایه‌ی خالی مقدار دهی کرده‌ایم. سپس برای مقدار دهی متغیر results موجود در state، به متد setResults تعریف شده‌ی توسط useState، مقدار response.data را ارسال می‌کنیم. در این حالت اگر برنامه را ذخیره کرده و اجرا کنید .... برنامه و همچنین مرورگر، هنگ می‌کنند!


همانطور که مشاهده می‌کنید، یک حلقه‌ی بی پایان در اینجا رخ داده‌است! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامه‌ی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرنده‌ی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا می‌شود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننت‌های کلاسی را با هم پوشش می‌دهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شده‌است و پس از آن، مجددا effect method فراخوانی می‌شود و ... این حلقه هیچ‌گاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ می‌کنند!

در این برنامه فعلا می‌خواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایه‌ی خالی مقدار دهی می‌کنیم:
  useEffect(() => {
   // ...
  }, []);
تا اینجا موفق شدیم متد setResults را تنها در اولین بار نمایش کامپوننت، فراخوانی کنیم که در نتیجه‌ی آن، متغیر results موجود در state، مقدار دهی شده و همچنین کار رندر مجدد کامپوننت در صف قرار می‌گیرد. بنابراین مرحله‌ی بعد، تکمیل قسمت return کامپوننت تابعی است تا آرایه‌ی results را نمایش دهد:
//...

export default function App() {
  // ...
  return (
    <>
      <table className="table">
        <thead>
          <tr>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {results.map(post => (
            <tr key={post.id}>
              <td>{post.title}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
در اینجا ابتدا یک فرگمنت را توسط </><> تعریف کرده‌ایم و سپس در داخل آن می‌توان المان‌های فرزند را قرار داد. سپس برای ایجاد trهای جدول، یک حلقه را توسط results.map، بر روی عناصر دریافتی از آرایه‌ی مطالب، تشکیل داده‌ایم. چون این حلقه بر روی trهای پویا تشکیل می‌شود، هر tr، نیاز به یک key دارد، تا در DOM مجازی React قابل شناسایی و ردیابی شود که در آخر یک چنین شکلی را ایجاد می‌کند:



استفاده ازAsync/Await  برای دریافت اطلاعات، درون یک  useEffect Hook

اکنون می‌خواهیم درون effect method یک useEffect Hook، روش قدیمی استفاده‌ی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آن‌را بیشتر بررسی کردیم، جایگزین کنیم.
  useEffect(async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  }, []);
خروجی متد axios.get، یک شیء Promise است که نتیجه‌ی عملیات async را بازگشت می‌دهد. در جاوا اسکریپت مدرن، می‌توان از واژه‌ی کلیدی await برای دسترسی به شیء response دریافتی از آن، استفاده کرد. سپس هر جائیکه از واژه‌ی کلیدی await استفاده می‌شود، متد جاری را باید با واژه‌ی کلیدی async نیز مزین کرد. با انجام اینکار و اجرای برنامه، اخطار زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود؛ هرچند نتیجه نهایی هم هنوز نمایش داده می‌شود:
Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise.
این اخطار به این معنا است که effect function تعریف شده را نمی‌توان به صورت async تعریف کرد و از چنین قابلیتی پشتیبانی نمی‌شود. یک effect function حداکثر می‌تواند یک متد دیگر را بازگشت دهد (و یا هیچ چیزی را بازگشت ندهد) که نمونه‌ی آن‌را در قسمت 30، با متدهایی که کار پاکسازی منابع را انجام می‌دادند، بررسی کردیم. اگر متدی را مزین به واژه‌ی کلیدی async کردیم، یعنی این متد در اصل یک Promise را بازگشت می‌دهد؛ اما یک effect function، حداکثر یک تابع دیگر را می‌تواند بازگشت دهد تا componentWillUnmount را پیاده سازی کند.

برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  };
مشکل یا محدودیتی برای ایجاد متدهای async، در خارج از یک effect function وجود ندارد. به همین جهت اعمالی را که نیاز به Async/Await دارند، در این متدهای مجزا انجام داده و سپس می‌توان آن‌ها را درون effect function، به نحوی که ملاحظه می‌کنید، فراخوانی کرد. با این تغییر، هنوز هم اطلاعات نهایی، بدون مشکل دریافت می‌شوند، اما دیگر اخطاری در کنسول توسعه دهندگان مرورگر درج نخواهد شد.


پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا

تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آن‌را از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقه‌ی بی‌نهایت وارد نشود. اکنون این سؤال مطرح می‌شود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شده‌ی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آن‌را با استفاده از useState Hook و یک مقدار اولیه‌ی دلخواه تنظیم می‌کنیم:
export default function App() {
  // ...
  const [query, setQuery] = useState("Title");
سپس المان textbox زیر را هم به بالای المان جدول، اضافه می‌کنیم:
<input
  type="text"
  name="query"
  className="form-control my-3"
  placeholder="Search..."
  onChange={event => setQuery(event.target.value)}
  value={query}
/>
این کنترل توسط رویداد onChange، عبارت تایپ شده را به متد setQuery ارسال کرده و در نتیجه‌ی آن، کار تنظیم متغیر query در state کامپوننت جاری، صورت می‌گیرد. همچنین با تنظیم value={query}، سبب خواهیم شد تا این کنترل، به یک المان کنترل شده‌ی توسط state تبدیل شود و در ابتدای نمایش فرم، مقدار ابتدایی useState را نمایش دهد.
اکنون که متغیر query دارای مقدار شده‌است، می‌توان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get(
   `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
);
استفاده از تابع encodeURIComponent، سبب می‌شود تا اگر کاربر برای مثال "Text 1" را وارد کرد، فاصله‌ی بین دو عبارت، به درستی encode شده و یک کوئری مانند https://localhost:5001/api/posts/search?query=Title%201 به سمت سرور ارسال گردد.

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمی‌شود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get می‌شود، طوری تنظیم شده‌است که فقط یکبار، آن‌هم پس از رندر اولیه‌ی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایه‌ای که به عنوان پارامتر دوم متد useEffect تعریف شده، می‌توان اجرای مجدد effect function آن‌را وابسته‌ی به تغییرات متغیر query در state کامپوننت کرد:
  useEffect(() => {
    getResults();
  }, [query]);
اکنون اگر برنامه را ذخیره کرده و اجرا کنید، با هربار ورود اطلاعات درون textbox جستجو، یک کوئری جدید به سمت سرور ارسال شده و نتیجه‌ی جستجوی انجام شده، به صورت یک جدول رندر می‌شود:



دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور


تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد می‌کند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن می‌توان یک دکمه‌ی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف می‌کنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آن‌را مجددا به یک آرایه‌ی خالی تنظیم می‌کنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره می‌کند، در بالای جدول نتایج مطالب، قرار می‌دهیم:
<button
  className="btn btn-primary"
  type="button"
  onClick={getResults}
>
  Search
</button>
تا اینجا اگر کاربر اطلاعاتی را وارد کرده و سپس بر روی دکمه‌ی Search فوق کلیک کند، نتایج جستجوی خودش را در جدول ذیل آن مشاهده می‌کند. اکنون می‌خواهیم این امکان را به کاربران بدهیم که با فشردن دکمه‌ی enter درون textbox جستجو، همین قابلیت جستجو را در اختیار داشته باشند؛ تا دیگر الزامی به کلیک بر روی دکمه‌ی Search، نباشد. برای اینکار تنها کافی است، کل مجموعه‌ی textbox و دکمه را درون یک المان form قرار دهیم و نوع button را نیز به submit تغییر دهیم. سپس onClick دکمه را حذف کرده و بجای آن رویداد onSubmit فرم را پیاده سازی می‌کنیم:
<form onSubmit={handleSearch}>
  <div className="input-group my-3">
    <label htmlFor="query" className="form-control-label sr-only"></label>
    <input
type="text"
id="query"
name="query"
className="form-control"
placeholder="Search ..."
onChange={event => setQuery(event.target.value)}
value={query}
    />
    <div className="input-group-append">
<button className="btn btn-primary" type="submit">
  Search
</button>
    </div>
  </div>
</form>
در اینجا یک المان فرم، به همراه یک textbox و button از نوع submit تعریف شده‌اند. رویداد onSubmit نیز به متد منتسب به متغیر handleSearch، متصل شده‌است تا با فشردن دکمه‌ی enter توسط کاربر در این textbox، کار جستجوی مجدد، صورت گیرد:
  const handleSearch = event => {
    event.preventDefault();
    getResults();
  };
تا اینجا اگر برنامه را ذخیره کرده و "Text 1" را در textbox جستجو، وارد کرده و enter کنیم، همانند تصویر فوق، رکورد متناظری از سرور دریافت و نمایش داده می‌شود.


افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook

در ادامه می‌خواهیم یک دکمه‌ی جدید را در کنار دکمه‌ی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شده‌ی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشته‌ی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آن‌را با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم». برای انجام یک چنین کاری در اینجا، Hook ویژه‌ای به نام useRef معرفی شده‌است. بنابراین برای پیاده سازی این نیازمند‌ی‌ها، ابتدا دکمه‌ی Clear را در کنار دکمه‌ی Search قرار می‌دهیم:
<button
  type="button"
  onClick={handleClearSearch}
  className="btn btn-info"
>
  Clear
</button>
سپس رویداد onClick آن‌را به متد منتسب به متغیر handleClearSearch، مرتبط می‌کنیم:
import React, { useEffect, useRef, useState } from "react";
// ...

export default function App() {
  // ...
  const searchInputRef = useRef();


  const handleClearSearch = () => {
    setQuery("");
    searchInputRef.current.focus();
  };
در اینجا ابتدا useRef را import کرده‌ایم، تا توسط آن بتوان یک متغیر از نوع React.MutableRefObject را ایجاد کرد. سپس در متد منتسب به handleClearSearch، ابتدا با فراخوانی setQuery، مقدار query را در state کامپوننت، پاک کرده و سپس به کمک این شیء Ref، دسترسی مستقیمی به شیء textbox یافته و متد focus آن‌را فراخوانی می‌کنیم (شیء current آن، معادل DOM Element متناظر است).
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آن‌را به ویژگی ref المان، انتساب داد:
<input
  type="text"
  id="query"
  name="query"
  className="form-control"
  placeholder="Search ..."
  onChange={event => setQuery(event.target.value)}
  value={query}
  ref={searchInputRef}
/>
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنیم، با کلیک بر روی دکمه‌ی Clear، متن textbox جستجو حذف شده و سپس کرسر مجددا به همان textbox برای ورود اطلاعات، منتقل می‌شود.


نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور


البته در اینجا با هر بار کلیک بر روی دکمه‌ی جستجو، نتیجه‌ی نهایی به سرعت نمایش داده می‌شود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ می‌دهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه می‌کنیم:
const [loading, setLoading] = useState(false);
تا با فراخوانی متد setLoading آن بتوان سبب رندر مجدد UI شد و پیامی را نمایش داد و یا مخفی کرد:
  const getResults = async () => {
    setLoading(true);
    const { data } = await axios.get(
      `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
    );
    console.log(data);
    setResults(data);
    setLoading(false);
  };
متد setLoading در ابتدای متد منتسب به متغیر getResults، مقدار متغیر loading را در state به true تنظیم می‌کند و در پایان عملیات، به false. اکنون بر این اساس می‌توان UI متناظری را نمایش داد:
      {loading ? (
        <div className="alert alert-info">Loading results...</div>
      ) : (
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
            </tr>
          </thead>
          <tbody>
            {results.map(post => (
              <tr key={post.id}>
                <td>{post.title}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
در اینجا با استفاده از یک ternary operator، اگر loading به true تنظیم شده باشد، یک div به همراه عبارت Loading results، نمایش داده می‌شود؛ در غیراینصورت، جدول اطلاعات مطالب، نمایش داده خواهد شد.

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



مدیریت خطاها در حین اعمال async

آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت می‌گیرد:
// ...

export default function App() {
  // ...
  const [error, setError] = useState(null);

  // ...

  const getResults = async () => {
    setLoading(true);

    try {
      const { data } = await axios.get(
        `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
      );
      console.log(data);
      setResults(data);
    } catch (err) {
      setError(err);
    }

    setLoading(false);
  };
در حین فراخوانی await axios.get، اگر خطایی رخ دهد، این کتابخانه استثنایی را صادر خواهد کرد که می‌توان به جزئیات آن در بدنه‌ی catch نوشته شده دسترسی یافت و برای مثال آن‌را به کاربر نمایش داد. برای این منظور ابتدا state مخصوص آن‌را ایجاد می‌کنیم و سپس توسط فراخوانی متد setError آن، کار رندر مجدد کامپوننت را در صف انجام قرار خواهیم داد.در نهایت برای نمایش آن می‌توان یک div را به پایین جدول اضافه نمود:
{error && <div className="alert alert-warning">{error.message}</div>}
برای آزمایش آن، برنامه‌ی backend را که در حال اجرا است، خاتمه دهید و سپس در برنامه سعی کنید به آن متصل شوید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip