From 14265164a42ca781df6909af7a15328fdbd10d6d Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Wed, 2 Nov 2022 21:48:22 +0100 Subject: [PATCH] Feature/swr for simple queries (#187) * Refactor simple queries with useSWR hook * Fix plus button in Extensions screen * Add type to categories query * Add useQuery abstraction which adds loading attribute * Reload categories even if reorder fails --- .../navbar/action/CategorySelect.tsx | 56 ++++------- src/screens/DownloadQueue.tsx | 19 +--- src/screens/Extensions.tsx | 95 ++++++++----------- src/screens/Library.tsx | 16 ++-- src/screens/Manga.tsx | 7 +- src/screens/SourceConfigure.tsx | 17 +--- src/screens/Sources.tsx | 23 ++--- src/screens/settings/About.tsx | 14 +-- src/screens/settings/Backup.tsx | 2 +- src/screens/settings/Categories.tsx | 52 +++++----- src/util/client.tsx | 15 +++ 11 files changed, 134 insertions(+), 182 deletions(-) diff --git a/src/components/navbar/action/CategorySelect.tsx b/src/components/navbar/action/CategorySelect.tsx index da149de9..0c9e38a7 100644 --- a/src/components/navbar/action/CategorySelect.tsx +++ b/src/components/navbar/action/CategorySelect.tsx @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import Button from '@mui/material/Button'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; @@ -14,7 +14,7 @@ import Dialog from '@mui/material/Dialog'; import Checkbox from '@mui/material/Checkbox'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormGroup from '@mui/material/FormGroup'; -import client from 'util/client'; +import client, { useQuery } from 'util/client'; interface IProps { open: boolean @@ -22,37 +22,21 @@ interface IProps { mangaId: number } -interface ICategoryInfo { - category: ICategory - selected: boolean -} - export default function CategorySelect(props: IProps) { const { open, setOpen, mangaId } = props; - const [categoryInfos, setCategoryInfos] = useState([]); - const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack - const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack + const { data: mangaCategoriesData, mutate } = useQuery(`/api/v1/manga/${mangaId}/category`); + const { data: categoriesData } = useQuery('/api/v1/category'); - useEffect(() => { - let tmpCategoryInfos: ICategoryInfo[] = []; - client.get('/api/v1/category/') - .then((response) => response.data) - .then((data: ICategory[]) => { - if (data.length > 0 && data[0].name === 'Default') { data.shift(); } - tmpCategoryInfos = data.map((category) => ({ category, selected: false })); - }) - .then(() => { - client.get(`/api/v1/manga/${mangaId}/category/`) - .then((response) => response.data) - .then((data: ICategory[]) => { - data.forEach((category) => { - tmpCategoryInfos[category.order - 1].selected = true; - }); - setCategoryInfos(tmpCategoryInfos); - }); - }); - }, [updateTriggerHolder, open]); + const allCategories = useMemo(() => { + const cats = [...(categoriesData ?? [])]; // make copy + if (cats.length > 0 && cats[0].name === 'Default') { + cats.shift(); // remove first category if it is 'Default' + } + return cats; + }, [categoriesData]); + + const selectedIds = mangaCategoriesData?.map((c) => c.id) ?? []; const handleCancel = () => { setOpen(false); @@ -67,7 +51,7 @@ export default function CategorySelect(props: IProps) { const method = checked ? client.get : client.delete; method(`/api/v1/manga/${mangaId}/category/${categoryId}`) - .then(() => triggerUpdate()); + .then(() => mutate()); }; return ( @@ -84,7 +68,7 @@ export default function CategorySelect(props: IProps) { Set categories - {categoryInfos.length === 0 + {allCategories.length === 0 && ( No categories found! @@ -92,17 +76,17 @@ export default function CategorySelect(props: IProps) { You should make some from settings. )} - {categoryInfos.map((categoryInfo) => ( + {allCategories.map((category) => ( handleChange(e, categoryInfo.category.id)} + checked={selectedIds.includes(category.id)} + onChange={(e) => handleChange(e, category.id)} color="default" /> )} - label={categoryInfo.category.name} - key={categoryInfo.category.id} + label={category.name} + key={category.id} /> ))} diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index 51954851..9b8e408a 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -9,7 +9,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import NavbarContext from 'components/context/NavbarContext'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; import IconButton from '@mui/material/IconButton'; @@ -28,8 +28,7 @@ import EmptyView from 'components/util/EmptyView'; import { Box } from '@mui/system'; import Typography from '@mui/material/Typography'; import { useHistory } from 'react-router-dom'; - -const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws'); +import useSubscription from 'components/library/useSubscription'; const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({ @@ -47,9 +46,8 @@ const initialQueue = { } as IQueue; export default function DownloadQueue() { - const [, setWsClient] = useState(); - const [queueState, setQueueState] = useState(initialQueue); - const { queue, status } = queueState; + const { data: queueState } = useSubscription('/api/v1/downloads'); + const { queue, status } = queueState ?? initialQueue; const history = useHistory(); @@ -84,15 +82,6 @@ export default function DownloadQueue() { }); }, [status]); - useEffect(() => { - const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`); - wsc.onmessage = (e) => { - setQueueState(JSON.parse(e.data)); - }; - - setWsClient(wsc); - }, []); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const onDragEnd = (result: DropResult) => { }; diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index 887e95b8..eb043247 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -5,13 +5,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { + useContext, useEffect, useState, useMemo, useRef, +} from 'react'; import { fromEvent } from 'file-selector'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; import ExtensionCard from 'components/ExtensionCard'; import NavbarContext from 'components/context/NavbarContext'; -import client from 'util/client'; +import client, { useQuery } from 'util/client'; import useLocalStorage from 'util/useLocalStorage'; import LangSelect from 'components/navbar/action/LangSelect'; import { extensionDefaultLangs, langCodeToName, langSortCmp } from 'util/language'; @@ -63,6 +65,7 @@ function groupExtensions(extensions: IExtension[]) { } export default function MangaExtensions() { + const inputRef = useRef(null); const { setTitle, setAction } = useContext(NavbarContext); const [shownLangs, setShownLangs] = useLocalStorage('shownExtensionLangs', extensionDefaultLangs()); const [showNsfw] = useLocalStorage('showNsfw', true); @@ -76,9 +79,7 @@ export default function MangaExtensions() { <> document.getElementById('external-extension-file')?.click() - } + onClick={() => inputRef.current?.click()} size="large" > @@ -92,16 +93,19 @@ export default function MangaExtensions() { ); }, [shownLangs]); - const [extensionsRaw, setExtensionsRaw] = useState([]); + const { data: allExtensions, mutate, loading } = useQuery('/api/v1/extension/list'); - const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack - const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack + const filteredExtensions = useMemo(() => (allExtensions ?? []).filter((ext) => { + const nsfwFilter = showNsfw || !ext.isNsfw; + if (!query) return nsfwFilter; + return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase()); + }), [allExtensions, showNsfw, query]); - useEffect(() => { - client.get('/api/v1/extension/list') - .then((response) => response.data) - .then((data) => setExtensionsRaw(data)); - }, [updateTriggerHolder]); + const groupedExtensions = useMemo(() => groupExtensions(filteredExtensions) + .filter((group) => group[EXTENSIONS].length > 0) + .filter((group) => ['installed', 'updates pending', 'all', ...shownLangs].includes(group[LANGUAGE])), [shownLangs, filteredExtensions]); + + const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2); const [toasts, makeToast] = makeToaster(useState([])); @@ -110,16 +114,16 @@ export default function MangaExtensions() { const formData = new FormData(); formData.append('file', file); - // empty the input - // @ts-ignore - document.getElementById('external-extension-file').value = null; + if (inputRef.current) { + inputRef.current.value = ''; + } makeToast('Installing Extension File....', 'info'); client.post('/api/v1/extension/install', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) .then(() => { makeToast('Installed extension successfully!', 'success'); - triggerUpdate(); + mutate(); }) .catch(() => makeToast('Extension installion failed!', 'error')); } else { @@ -127,60 +131,43 @@ export default function MangaExtensions() { } }; - const dropHandler = async (e: Event) => { - e.preventDefault(); - const files = await fromEvent(e); - - submitExternalExtension(files[0] as File); - }; - - const dragOverHandler = (e: Event) => { - e.preventDefault(); - }; - useEffect(() => { - document.addEventListener('drop', dropHandler); - document.addEventListener('dragover', dragOverHandler); - - const changeHandler = async (evt: Event) => { - const files = await fromEvent(evt); + const dropHandler = async (e: Event) => { + e.preventDefault(); + const files = await fromEvent(e); submitExternalExtension(files[0] as File); }; - const input = document.getElementById('external-extension-file'); - input?.addEventListener('change', changeHandler); + + const dragOverHandler = (e: Event) => { + e.preventDefault(); + }; + + document.addEventListener('drop', dropHandler); + document.addEventListener('dragover', dragOverHandler); return () => { document.removeEventListener('drop', dropHandler); document.removeEventListener('dragover', dragOverHandler); - input?.removeEventListener('change', changeHandler); }; - }, [extensionsRaw]); // useEffect only after renders + }, []); - if (extensionsRaw.length === 0) { + if (loading) { return ; } - const filtered = extensionsRaw.filter((ext) => { - const nsfwFilter = showNsfw || !ext.isNsfw; - if (!query) return nsfwFilter; - return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase()); - }); - - const combinedShownLangs = ['installed', 'updates pending', 'all', ...shownLangs]; - - const groupedExtensions: [string, IExtension[]][] = groupExtensions(filtered) - .filter((group) => group[EXTENSIONS].length > 0) - .filter((group) => combinedShownLangs.includes(group[LANGUAGE])); - - const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2); - return ( <> {toasts} { + const file = e.target.files?.[0]; + if (file) { + submitExternalExtension(file); + } + }} /> { - triggerUpdate(); + mutate(); }} /> ); diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index 7092ea25..18882639 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -17,17 +17,21 @@ import LibraryOptions from 'components/library/LibraryOptions'; import LibraryMangaGrid from 'components/library/LibraryMangaGrid'; import AppbarSearch from 'components/util/AppbarSearch'; import { useQueryParam, NumberParam } from 'use-query-params'; -import useSWR from 'swr'; +import { useQuery } from 'util/client'; import UpdateChecker from '../components/library/UpdateChecker'; export default function Library() { - const { data: tabsData, error: tabsError } = useSWR('/api/v1/category'); + const { data: tabsData, error: tabsError, loading } = useQuery('/api/v1/category'); const tabs = tabsData ?? []; const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam); const activeTab = tabs.find((t) => t.order === tabSearchParam) ?? tabs[0]; - const { data: mangaData, error: mangaError } = useSWR(`/api/v1/category/${activeTab?.id}`, { + const { + data: mangaData, + error: mangaError, + loading: mangaLoading, + } = useQuery(`/api/v1/category/${activeTab?.id}`, { isPaused: () => activeTab == null, }); const mangas = mangaData ?? []; @@ -56,7 +60,7 @@ export default function Library() { return ; } - if (tabsData == null) { + if (loading) { return ; } @@ -72,7 +76,7 @@ export default function Library() { lastPageNum={lastPageNum} setLastPageNum={setLastPageNum} message="Your Library is empty" - isLoading={activeTab != null && mangaData == null && mangaError == null} + isLoading={activeTab != null && mangaLoading} /> ); } @@ -110,7 +114,7 @@ export default function Library() { lastPageNum={lastPageNum} setLastPageNum={setLastPageNum} message="Category is Empty" - isLoading={mangaData == null} + isLoading={mangaLoading} /> ))} diff --git a/src/screens/Manga.tsx b/src/screens/Manga.tsx index 907aa012..526c52b2 100644 --- a/src/screens/Manga.tsx +++ b/src/screens/Manga.tsx @@ -8,11 +8,10 @@ import React, { useCallback, useEffect, useContext, useState, useRef, } from 'react'; -import useSWR from 'swr'; import { Box } from '@mui/system'; import MangaDetails from 'components/MangaDetails'; import NavbarContext from 'components/context/NavbarContext'; -import { fetcher } from 'util/client'; +import { fetcher, useQuery } from 'util/client'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import ChapterList from 'components/chapter/ChapterList'; import { useParams } from 'react-router-dom'; @@ -24,8 +23,8 @@ export default function Manga() { const { id } = useParams<{ id: string }>(); const autofetchedRef = useRef(false); - const { data: manga, error, mutate: mutateManga } = useSWR(`/api/v1/manga/${id}/?onlineFetch=false`); - const { data: chaptersData, mutate: mutateChapters } = useSWR(`/api/v1/manga/${id}/chapters?onlineFetch=false`); + const { data: manga, error, mutate: mutateManga } = useQuery(`/api/v1/manga/${id}/?onlineFetch=false`); + const { data: chaptersData, mutate: mutateChapters } = useQuery(`/api/v1/manga/${id}/chapters?onlineFetch=false`); const [fetchingOnline, setFetchingOnline] = useState(false); const fetchOnline = useCallback(async () => { diff --git a/src/screens/SourceConfigure.tsx b/src/screens/SourceConfigure.tsx index 8795396e..7ed25d23 100644 --- a/src/screens/SourceConfigure.tsx +++ b/src/screens/SourceConfigure.tsx @@ -5,10 +5,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import NavbarContext from 'components/context/NavbarContext'; import { useParams } from 'react-router-dom'; -import client from 'util/client'; +import client, { useQuery } from 'util/client'; import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference'; import ListPreference from 'components/sourceConfiguration/ListPreference'; import EditTextPreference from 'components/sourceConfiguration/EditTextPreference'; @@ -34,21 +34,12 @@ function getPrefComponent(type: string) { } export default function SourceConfigure() { - const [sourcePreferences, setSourcePreferences] = useState([]); const { setTitle, setAction } = useContext(NavbarContext); - const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack - const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack - useEffect(() => { setTitle('Source Configuration'); setAction(<>); }, []); const { sourceId } = useParams<{ sourceId: string }>(); - - useEffect(() => { - client.get(`/api/v1/source/${sourceId}/preferences`) - .then((response) => response.data) - .then((data) => setSourcePreferences(data)); - }, [updateTriggerHolder]); + const { data: sourcePreferences = [], mutate } = useQuery(`/api/v1/source/${sourceId}/preferences`); const convertToString = (position: number, value: any): string => { switch (sourcePreferences[position].props.defaultValueType) { @@ -63,7 +54,7 @@ export default function SourceConfigure() { (value: any) => { client.post(`/api/v1/source/${sourceId}/preferences`, JSON.stringify({ position, value: convertToString(position, value) })) - .then(() => triggerUpdate()); + .then(() => mutate()); } ); diff --git a/src/screens/Sources.tsx b/src/screens/Sources.tsx index 1cd005bb..7bd40985 100644 --- a/src/screens/Sources.tsx +++ b/src/screens/Sources.tsx @@ -5,11 +5,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import LangSelect from 'components/navbar/action/LangSelect'; import SourceCard from 'components/SourceCard'; import NavbarContext from 'components/context/NavbarContext'; -import client from 'util/client'; import { sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp, } from 'util/language'; @@ -18,6 +17,7 @@ import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; import { IconButton } from '@mui/material'; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import { useHistory } from 'react-router-dom'; +import { useQuery } from 'util/client'; function sourceToLangList(sources: ISource[]) { const result: string[] = []; @@ -46,8 +46,7 @@ export default function Sources() { const [shownLangs, setShownLangs] = useLocalStorage('shownSourceLangs', sourceDefualtLangs()); const [showNsfw] = useLocalStorage('showNsfw', true); - const [sources, setSources] = useState([]); - const [fetched, setFetched] = useState(false); + const { data: sources, loading } = useQuery('/api/v1/source/list'); const history = useHistory(); @@ -80,27 +79,23 @@ export default function Sources() { , ); }, [shownLangs, sources]); - useEffect(() => { - client.get('/api/v1/source/list') - .then((response) => response.data) - .then((data) => { setSources(data); setFetched(true); }); - }, []); + if (loading) return ; - if (sources.length === 0) { - if (fetched) return (

No sources found. Install Some Extensions first.

); - return ; + if (sources?.length === 0) { + return (

No sources found. Install Some Extensions first.

); } + return ( <> {/* eslint-disable-next-line max-len */} - {Object.entries(groupByLang(sources)).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => ( + {Object.entries(groupByLang(sources ?? [])).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => ( shownLangs.indexOf(lang) !== -1 && (

{langCodeToName(lang)}

diff --git a/src/screens/settings/About.tsx b/src/screens/settings/About.tsx index d202a873..c75a379f 100644 --- a/src/screens/settings/About.tsx +++ b/src/screens/settings/About.tsx @@ -1,26 +1,18 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; -import client from 'util/client'; import ListItemLink from 'components/util/ListItemLink'; import NavbarContext from 'components/context/NavbarContext'; import LoadingPlaceholder from 'components/util/LoadingPlaceholder'; +import { useQuery } from 'util/client'; export default function About() { const { setTitle, setAction } = useContext(NavbarContext); - const [about, setAbout] = useState(); - useEffect(() => { setTitle('About'); setAction(<>); }, []); - useEffect(() => { - client.get('/api/v1/settings/about') - .then((response) => response.data) - .then((data:IAbout) => { - setAbout(data); - }); - }, []); + const { data: about } = useQuery('/api/v1/settings/about'); if (about === undefined) { return ; diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index 9f9ffa3c..a6f75a1e 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -10,10 +10,10 @@ import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import { fromEvent } from 'file-selector'; +import client from 'util/client'; import makeToast from 'components/util/Toast'; import ListItemLink from '../../components/util/ListItemLink'; import NavbarContext from '../../components/context/NavbarContext'; -import client from '../../util/client'; export default function Backup() { const { setTitle, setAction } = useContext(NavbarContext); diff --git a/src/screens/settings/Categories.tsx b/src/screens/settings/Categories.tsx index 4a432e59..149061e7 100644 --- a/src/screens/settings/Categories.tsx +++ b/src/screens/settings/Categories.tsx @@ -8,7 +8,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useState, useContext, useEffect } from 'react'; +import React, { + useMemo, useState, useContext, useEffect, +} from 'react'; import { List, ListItem, @@ -34,7 +36,7 @@ import DialogTitle from '@mui/material/DialogTitle'; import Checkbox from '@mui/material/Checkbox'; import FormControlLabel from '@mui/material/FormControlLabel'; import NavbarContext from 'components/context/NavbarContext'; -import client from 'util/client'; +import client, { useQuery } from 'util/client'; const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({ @@ -50,37 +52,31 @@ export default function Categories() { const { setTitle, setAction } = useContext(NavbarContext); useEffect(() => { setTitle('Categories'); setAction(<>); }, []); - const [categories, setCategories] = useState([]); + const { data, mutate } = useQuery('/api/v1/category/'); + const categories = useMemo(() => { + const res = [...data ?? []]; + if (res.length > 0 && res[0].name === 'Default') { + res.shift(); + } + return res; + }, [data]); + const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category const [dialogOpen, setDialogOpen] = useState(false); const [dialogName, setDialogName] = useState(''); const [dialogDefault, setDialogDefault] = useState(false); const theme = useTheme(); - const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack - const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack - - useEffect(() => { - if (!dialogOpen) { - client.get('/api/v1/category/') - .then((response) => response.data) - .then((data) => { if (data.length > 0 && data[0].name === 'Default') data.shift(); return data; }) - .then((data) => setCategories(data)); - } - }, [updateTriggerHolder]); - const categoryReorder = (list: ICategory[], from: number, to: number) => { + const newData = [...list]; + const [removed] = newData.splice(from, 1); + newData.splice(to, 0, removed); + mutate(newData, { revalidate: false }); + const formData = new FormData(); formData.append('from', `${from + 1}`); formData.append('to', `${to + 1}`); - client.patch('/api/v1/category/reorder', formData) - .finally(() => triggerUpdate()); - - // also move it in local state to avoid jarring moving behviour... - const result = Array.from(list); - const [removed] = result.splice(from, 1); - result.splice(to, 0, removed); - return result; + client.patch('/api/v1/category/reorder', formData).finally(() => mutate()); }; const onDragEnd = (result: DropResult) => { @@ -89,11 +85,11 @@ export default function Categories() { return; } - setCategories(categoryReorder( + categoryReorder( categories, result.source.index, result.destination.index, - )); + ); }; const resetDialog = () => { @@ -127,18 +123,18 @@ export default function Categories() { if (categoryToEdit === -1) { client.post('/api/v1/category/', formData) - .finally(() => triggerUpdate()); + .finally(() => mutate()); } else { const category = categories[categoryToEdit]; client.patch(`/api/v1/category/${category.id}`, formData) - .finally(() => triggerUpdate()); + .finally(() => mutate()); } }; const deleteCategory = (index:number) => { const category = categories[index]; client.delete(`/api/v1/category/${category.id}`) - .finally(() => triggerUpdate()); + .finally(() => mutate()); }; return ( diff --git a/src/util/client.tsx b/src/util/client.tsx index f7b6f611..b55e00db 100644 --- a/src/util/client.tsx +++ b/src/util/client.tsx @@ -6,6 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import axios from 'axios'; +import useSWR, { SWRConfiguration, SWRResponse } from 'swr'; import storage from './localStorage'; const { hostname, port, protocol } = window.location; @@ -40,3 +41,17 @@ export async function fetcher(path: string) { } return res.data as T; } + +export const useQuery = < + D extends any = any, + E extends any = any, +>( + key: string, + config?: SWRConfiguration, +): SWRResponse & { loading: boolean } => { + const res = useSWR(key, config); + return { + ...res, + loading: res.data == null && res.error == null, + }; +};