رابط کاربری خوشبینانه: افزایش تجربه کاربر در React با React Query

در توسعه وب مدرن، ارائه یک تجربه کاربری روان و پاسخگو بسیار مهم است. یکی از جنبه هایی که ممکن است توسط کاربر نهایی مورد توجه قرار نگیرد این است که چگونه برنامه داده های جهش یافته را مدیریت می کند، یعنی ایجاد، به روز رسانی و حذف.
این احتمالاً به دلیل دوگانگی ماهیت این ویژگی است: اگر خوب کار کند، هیچ کس متوجه نمی شود. اگر اینطور نباشد، برنامه بد در نظر گرفته می شود.
کاربران می خواهند احساس کنند که بر کاری که انجام می دهند کنترل دارند. تقریباً هر حالت بارگیری در برنامه شما این پتانسیل را دارد که حواس کاربر را پرت کند، او باید منتظر بماند تا عملیات تمام شود. اما برنامه ها قرار است اینگونه کار کنند، درست است؟
اگر برنامه شما جامد است و به خوبی کار می کند، احتمالاً متوجه شده اید که اکثر درخواست های جهش داده ها در سرور موفقیت آمیز هستند. حتی اگر اکثر برنامهها در حین جهش از حالتهای بارگیری استفاده میکنند، پس از دریافت پاسخ، دادههای بهروزرسانی شده را نشان میدهند.
امروز برات توضیح میدم چه گزینه هایی برای بهبود این جریان کاربری در دسترس دارید و چرا از آنها استفاده کنید. سپس نمونه کدهای هر روش را با پیاده سازی گام به گام به شما نشان خواهم داد. به خواندن ادامه دهید!
روش سنتی
پرکاربردترین و سنتی ترین روش جهش داده ها، رابط کاربری بدبینانه است.
UI بدبینانه یک رویکرد در طراحی UI/UX است که در آن تغییرات ایجاد شده توسط کاربر بلافاصله در صفحه نمایش منعکس نمی شود تا زمانی که پاسخ سرور دریافت و تایید شود.
هنگامی که کاربران اقدامی مانند ارسال فرم یا زدن یک دکمه را انجام میدهند، این رابط تا زمانی که پاسخی از سرور دریافت نشود، بدون تغییر باقی میماند. در این مدت کاربر ممکن است یک نشانگر بارگذاری و دکمه ها یا ورودی های مسدود شده را ببیند. وقتی پاسخ دریافت شد، رابط کاربری با داده های جدید به روز می شود یا بازخورد داده می شود.
این رویکرد بدبینانه در شرایطی به کار می رود و مفید است سازگاری و دقت داده ها مهم است. با به تعویق انداختن بهروزرسانیهای رابط کاربری تا زمانی که پاسخ تأیید شود، شانس نمایش دادههای نادرست و متناقض کاهش مییابد.
برخی از نمونههای استفاده از رابط کاربری بدبینانه، تراکنشهای بانکی و ورود به سیستم و ثبتنام برنامهها هستند، که در آن امنیت و ثبات به یک رابط پاسخگو ترجیح داده میشود.
اما گاهی اوقات لازم است به کاربر حس کنترل بیشتری بدهیم و همچنین تجربه نرمافزاری نرمافزاری را ارائه کنیم.
تصور کنید هر بار که یک پست اینستاگرام را لایک می کنید، یک اسپینر در حال بارگذاری را ببینید. این قطعا ایده آل نیست، و شما به طور کامل درگیر تجربه کامل برنامه نخواهید بود.
برای حل این مشکل، رابط کاربری Optimistic را داریم.
درک رابط کاربری خوش بینانه
رابط کاربری خوشبینانه رویکردی است که با هدف ارائه بازخورد فوری به کاربر، و سپس درخواست جهش را در پس زمینه فعال کنید.
هنگام انجام اقداماتی مانند کلیک کردن بر روی دکمه ها یا ارسال فرم ها، رابط قبل از ارسال درخواست واقعی به روز می شود، بنابراین کاربر می تواند سفر را از طریق برنامه ادامه دهد. در همین حال، سرور در حال پردازش درخواست است و در نهایت پاسخ را به فرانت اند می دهد. اگر پاسخ ناموفق باشد، بهروزرسانیهای رابط کاربری برگردانده میشوند، و اگر پاسخ موفقیتآمیز باشد، هیچ کاری لازم نیست انجام شود – UI قبلاً بهروزرسانی شده است.
رویکرد UI خوش بینانه در سناریوهایی استفاده می شود که در آن بازخورد و پاسخگویی بسیار مهم است. هر گونه تاخیر درک شده را حذف می کند و تجربه کاربری روان تری ایجاد می کند با فرض یک پاسخ موفقیت آمیز از قبل
همانطور که در بالا ذکر شد، یک نمونه از رابط کاربری Optimistic دکمه لایک اینستاگرام است. هنگامی که دکمه کلیک می شود (پسندیدن یا عدم دوست داشتن)، رابط کاربری از قبل به روز می شود و سپس درخواست ارسال می شود. این نیز عالی است زیرا برخی از کاربران ممکن است از یک شبکه کند استفاده کنند، بنابراین Optimistic UI در این مورد یک حلال مشکل است.
اکنون که تفاوتها و موارد استفاده هر روش را متوجه شدید، بیایید به چند نمونه کد بپردازیم.
سناریوی نمونه های ما
قبل از ورود به کد، اجازه دهید به شما نشان دهم که در همه مثالها از چه چیزی استفاده خواهیم کرد.
بیایید یک برنامه ساده React را در نظر بگیریم که دارای دکمه “ارسال پیام” است که با کلیک روی آن درخواستی را به سرور ارسال می کند و سپس یک پیام جدید به لیست اضافه می شود. همچنین، هنگامی که کاربر وارد صفحه می شود، پیام های موجود باید از API واکشی شوند.
برای شبیه سازی درخواست API، یک وعده حل می شود و یک تاخیر به آن اضافه می شود، بنابراین می توانید رفتار کلی را بهتر درک کنید. در اینجا دو درخواست تمسخر آمیز API وجود دارد که قرار است از آنها استفاده کنیم
// Default existing messages
const messages = [
{
id: 1,
message: "Existing message",
},
{
id: 2,
message: "Existing message",
},
];
// Return all the messages
export const getMessages = () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(messages);
}, 1500);
});
// Add a new message to the messages array
export const sendMessage = (message) =>
new Promise((resolve) => {
setTimeout(() => {
messages.push(message);
resolve(messages);
}, 1500);
});
برای استایل ها، از TailwindCSS استفاده خواهم کرد.
همه نمونه ها در GitHub من هستند، بنابراین با خیال راحت مخزن را شبیه سازی کنید و با آن بازی کنید. تمام جزئیات نحوه اجرای آن در فایل README توضیح داده شده است.
با این گفته، بیایید به کدها بپریم!
به روز رسانی های بدبینانه بدون React Query
برای اینکه بتوانیم این کار را انجام دهیم، باید رفتار بارگذاری را مدیریت کنیم و پیام جدید را به حالت اضافه کنیم. این اساساً همان کاری است که اکثر برنامه ها در پشت صحنه انجام می دهند.
const PessimisticUpdate = () => {
const [isFetching, setIsFetching] = useState(false); // Used for initial fetching
const [isLoading, setIsLoading] = useState(false); // Used when sending a new message
const [messages, setMessages] = useState([]); // Local state to store messages
// Load existing messages from API when component mounts,
// and shows a loading state while fetching
useEffect(() => {
setIsFetching(true);
getMessages().then((messages) => {
setMessages(messages);
setIsFetching(false);
});
}, []);
const handleClick = () => {
setIsLoading(true); // Enable loading
const newMessage = {
id: messages.length + 1,
message: "New message",
};
// Dispatch API request to add the new message
sendMessage(newMessage).then((updatedMessages) => {
setMessages(updatedMessages); // Update messages in the local state
setIsLoading(false); // Disable loading
});
};
return (
<>
{!isFetching && <MessagesList messages={messages} />}
<Button
text="Send message"
onClick={handleClick}
isLoading={isLoading || isFetching}
/>
</>
);
};
توجه داشته باشید که بازخورد –isLoading
– فقط پس از حل شدن وعده تعریف می شود.
به روز رسانی های خوش بینانه بدون React Query
بزرگترین تفاوت در اینجا این است که ما نیاز نخواهیم داشت isLoading
دیگر، از آنجایی که از قبل در نظر داریم که هنگام ارسال پیام همه چیز خوب کار کند، بنابراین بازخورد رابط کاربری فوری خواهد بود و برای این کار نیازی به حالت بارگیری نخواهد بود.
const OptimisticUpdate = () => {
const [isFetching, setIsFetching] = useState(false); // Used for initial fetching
const [messages, setMessages] = useState([]); // Local state to keep messages
// Load existing messages from API when component mounts,
// and shows a loading state while fetching
useEffect(() => {
setIsFetching(true);
getMessages().then((messages) => {
setMessages(messages);
setIsFetching(false);
});
}, []);
const handleClick = () => {
const newMessage = {
id: messages.length + 1,
message: "New message",
};
// Update message in the local state upfront
setMessages((previousMessages) => [...previousMessages, newMessage]);
// Then dispatch API request to add new message
sendMessage(newMessage).then(() => {
console.log(`Message ${newMessage.id} added`);
});
};
return (
<>
<h1 className="font-bold text-xl mb-2 text-center">Optimistic UI</h1>
{!isFetching && <MessagesList messages={messages} />}
<Button
text="Send message"
onClick={handleClick}
isLoading={isFetching}
/>
</>
);
};
این یک تغییر بسیار کوچک برای یک پیشرفت بزرگ از نظر UX بود. هر چند برخی معاوضه ها وجود دارد.
اگر درخواست ناموفق باشد، ما در نظر نمی گیریم. حذف داده های نامعتبر از حالت در برخی موارد ساده نیست و باید به صورت دستی انجام شود. احتمالاً برای حفظ پیامهای موقت، به همراه وضعیت با پیامهای واقعی، باید حالت دیگری را اضافه کنید.
با توجه به اینکه یک برنامه واقعی چیزهای بیشتری دارد، باید مطمئن شوید که همه تغییرات رابط کاربری را نیز برگردانید و در صورت عدم موفقیت درخواست، اقدامات صحیح را انجام دهید. این ممکن است کمی مشکل باشد و ممکن است کمی پیچیده باشد.
به روز رسانی های بدبینانه با React Query
بزرگترین تفاوت در اینجا این است که React Query تمام حالت های مرتبط با پیام ها و درخواست ها را مدیریت می کند. به جای تماس مستقیم سرویس در کامپوننت، به React Hook منتقل می شود و بلافاصله فراخوانی می شود.
const PessimisticUpdateReactQuery = () => {
const cacheKey = "messages"; // Cache key of the messages
const queryClient = useQueryClient();
// Load existing messages from API
const { data: messages, isLoading: isFetching } = useQuery(
cacheKey,
getMessages
);
// Dispatch API request to add new message
const { mutateAsync, isLoading } = useMutation(sendMessage, {
onSuccess: () => {
// Invalidate messages cache, since a new message was added.
// The `messages` var will be automatically updated with fresh data.
queryClient.invalidateQueries(cacheKey);
},
});
const handleClick = () => {
const newMessage = {
id: messages.length + 1,
message: "New message",
};
// Call React Query hook that calls sendMessage
mutateAsync(newMessage);
};
return (
<>
{!isFetching && <MessagesList messages={messages} />}
<Button
text="Send message"
onClick={handleClick}
isLoading={isLoading || isFetching}
/>
</>
);
};
خواندن کد بسیار ساده تر است، زیرا بسیاری از چیزها توسط کتابخانه انتزاع می شوند.
همچنین میتوانید ذخیرهسازی خارج از جعبه را داشته باشید. اگر تماس دیگری با همان سرویس برقرار شود، قلاب React Query به جای برقراری تماس API جدید، مقدار کش شده را برمی گرداند و می توانید زمان کش را به دلخواه خود پیکربندی کنید. این یک بهبود عملکرد بزرگ است.
به روز رسانی های خوش بینانه با React Query
همانطور که در مثال بالا بحث شد، React Query با تمام وضعیت سرور سروکار دارد، و نیازی به مدیریت هیچکدام نخواهید داشت useState
از React.
در اینجا ما نسخه بهینه برای مقابله با به روز رسانی های خوش بینانه را داریم. تمام رفتارها تحت مسئولیت React Query است، بنابراین ما فقط باید API های آن را فراخوانی کنیم.
کد زیر ممکن است در ابتدا گیج کننده به نظر برسد، اما بیایید به آن بپردازیم و تمام تغییرات ایجاد شده را درک کنیم:
const OptimisticUpdateReactQuery = () => {
const cacheKey = "messages"; // Cache key of the messages
const queryClient = useQueryClient();
// Load existing messages from API
const { data: messages, isLoading: isFetching } = useQuery(
cacheKey,
getMessages
);
// Dispatch API request to add new message
// and do the optimistic update.
const { mutateAsync } = useMutation(sendMessage, {
onMutate: async (newMessage) => {
// Cancel any outgoing refetches (so they don't overwrite the optimistic update)
await queryClient.cancelQueries(cacheKey);
// Snapshot the previous value
const previousMessages = queryClient.getQueryData(cacheKey);
// Optimistically update to the new value
queryClient.setQueryData(cacheKey, (old) => [...old, newMessage]);
// Return a context object with the snapshotted value
return { previousMessages };
},
onError: (_, __, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(cacheKey, context.previousMessages);
},
onSettled: (messages) => {
// Always refetch after error or success:
queryClient.invalidateQueries(cacheKey);
console.log(`Message ${messages[messages.length - 1].id} added`);
},
});
const handleClick = () => {
const newMessage = {
id: messages.length + 1,
message: "New message",
};
mutateAsync(newMessage);
};
return (
<section>
{!isFetching && <MessagesList messages={messages} />}
<Button text="Send message" onClick={handleClick} />
</section>
);
}
همانطور که قبلا ذکر شد، این دارای مزایای زیادی مانند ذخیره سازی، باطل کردن خودکار و واکشی مجدد است.
یک ایده عالی در این مورد، انتزاع رفتار خوش بینانه است (زیر onMutate
) به یک قلاب سفارشی، پس فقط می توانید این قلاب را که دارای ویژگی خوش بینانه است صدا بزنید. قلاب می تواند چیزی شبیه به این باشد:
const useOptimisticUpdate = () => {
const queryClient = useQueryClient();
const getPreviousData = async ({ cacheKey, newValue }) => {
await queryClient.cancelQueries(cacheKey);
// Snapshot the previous value
const previousData = queryClient.getQueryData(cacheKey);
// Optimistically update to the new value
queryClient.setQueryData(cacheKey, (old) => [...old, newValue]);
// Return a context object with the snapshotted value
return { previousData };
};
return { getPreviousData };
};
و سپس تماس ها را جایگزین کنید onMutate
با قلاب:
const OptimisticUpdateReactQuery = () => {
// ...
const { getPreviousData } = useOptimisticUpdate(); // New hook call
const { mutateAsync } = useMutation(sendMessage, {
onMutate: async (newMessage) => ({
previousMessages: await getPreviousData({ // Get previous data from the hook
cacheKey,
newValue: newMessage,
})
}),
// ...
اکنون یک ویژگی بهروزرسانی خوشبینانه قابل استفاده مجدد دارید که میتواند در تمام اجزای مورد نیاز به اشتراک گذاشته شود.
بسته شدن
تجربیاتی که بسیاری از انواع برنامه ها ارائه می دهند ممکن است همیشه بر روی کاربر نهایی متمرکز باشد. اینجاست که UI خوش بینانه می درخشد.
به روز رسانی های خوش بینانه با ارائه بازخورد فوری و پاسخگویی در طول به روز رسانی داده ها، تجربه کاربر را به طور قابل توجهی بهبود می بخشد. در حالی که روشهای سنتی نیاز به مدیریت دستی حالت و مدیریت خطا دارند، React Query با ارائه توابع جهش داخلی، باطل کردن خودکار حافظه پنهان و مدیریت خطا، فرآیند را ساده میکند. با استفاده از React Query، میتوانید برنامهها را با تجربه کاربری روانتر ارتقا دهید، در زمان و تلاش در مدیریت بهروزرسانیهای دادهها به صورت دستی صرفهجویی کنید.
یکی از مواردی که باید در نظر گرفت این است که هر موردی با این مفهوم مطابقت ندارد، گاهی اوقات به دلایل زیادی مانند امنیت و ثبات داده ها بهتر است از رابط کاربری بدبین استفاده کنید.
در مورد کاربرد یک مفهوم یا مفهوم دیگر بسیار متفکر باشید و به بحث در مورد تصمیم با طراحان و PO فکر کنید. آنها می توانند با دیدگاه های دیگر مشارکت کنند زیرا این تصمیم به شدت بر اساس تجربه کاربر است.
اگر بازخورد یا پیشنهادی دارید، برای من ایمیل بفرستید
کد نویسی عالی!