From c1786f8e24ccf8cdf94040705e542bce35396398 Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Sun, 7 Mar 2021 22:25:29 +0330 Subject: [PATCH] migrate to axios, front-end part of configurable ServerAddress --- webUI/react/src/components/CategorySelect.tsx | 15 ++-- webUI/react/src/components/ExtensionCard.tsx | 15 ++-- webUI/react/src/components/MangaDetails.tsx | 5 +- webUI/react/src/screens/Extensions.tsx | 5 +- webUI/react/src/screens/Library.tsx | 7 +- webUI/react/src/screens/Manga.tsx | 9 ++- webUI/react/src/screens/Reader.tsx | 9 ++- webUI/react/src/screens/SearchSingle.tsx | 9 ++- webUI/react/src/screens/Settings.tsx | 78 +++++++++++++++++-- webUI/react/src/screens/SourceMangas.tsx | 9 ++- webUI/react/src/screens/Sources.tsx | 5 +- .../react/src/screens/settings/Categories.jsx | 52 +++++-------- webUI/react/src/util/client.tsx | 26 +++++-- .../util/{storage.tsx => localStorage.tsx} | 18 +++-- webUI/react/src/util/useLocalStorage.tsx | 34 ++------ 15 files changed, 182 insertions(+), 114 deletions(-) rename webUI/react/src/util/{storage.tsx => localStorage.tsx} (51%) diff --git a/webUI/react/src/components/CategorySelect.tsx b/webUI/react/src/components/CategorySelect.tsx index cf5feb6..8b51ddf 100644 --- a/webUI/react/src/components/CategorySelect.tsx +++ b/webUI/react/src/components/CategorySelect.tsx @@ -12,6 +12,7 @@ import Dialog from '@material-ui/core/Dialog'; import Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormGroup from '@material-ui/core/FormGroup'; +import client from '../util/client'; const useStyles = makeStyles(() => createStyles({ paper: { @@ -41,14 +42,14 @@ export default function CategorySelect(props: IProps) { useEffect(() => { let tmpCategoryInfos: ICategoryInfo[] = []; - fetch('http://127.0.0.1:4567/api/v1/category/') - .then((response) => response.json()) + client.get('/api/v1/category/') + .then((response) => response.data) .then((data: ICategory[]) => { tmpCategoryInfos = data.map((category) => ({ category, selected: false })); }) .then(() => { - fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`) - .then((response) => response.json()) + client.get(`/api/v1/manga/${mangaId}/category/`) + .then((response) => response.data) .then((data: ICategory[]) => { data.forEach((category) => { tmpCategoryInfos[category.order - 1].selected = true; @@ -69,9 +70,9 @@ export default function CategorySelect(props: IProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const handleChange = (event: React.ChangeEvent, categoryId: number) => { const { checked } = event.target as HTMLInputElement; - fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, { - method: checked ? 'GET' : 'DELETE', mode: 'cors', - }) + + const method = checked ? client.get : client.delete; + method(`/api/v1/manga/${mangaId}/category/${categoryId}`) .then(() => triggerUpdate()); }; diff --git a/webUI/react/src/components/ExtensionCard.tsx b/webUI/react/src/components/ExtensionCard.tsx index 26e7588..236c77f 100644 --- a/webUI/react/src/components/ExtensionCard.tsx +++ b/webUI/react/src/components/ExtensionCard.tsx @@ -9,6 +9,7 @@ import CardContent from '@material-ui/core/CardContent'; import Button from '@material-ui/core/Button'; import Avatar from '@material-ui/core/Avatar'; import Typography from '@material-ui/core/Typography'; +import client from '../util/client'; const useStyles = makeStyles((theme) => ({ root: { @@ -53,16 +54,18 @@ export default function ExtensionCard(props: IProps) { function install() { setInstalledState('installing'); - fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => { - setInstalledState('uninstall'); - }); + client.get(`/api/v1/extension/install/${apkName}`) + .then(() => { + setInstalledState('uninstall'); + }); } function uninstall() { setInstalledState('uninstalling'); - fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => { - setInstalledState('install'); - }); + client.get(`/api/v1/extension/uninstall/${apkName}`) + .then(() => { + setInstalledState('install'); + }); } function handleButtonClick() { diff --git a/webUI/react/src/components/MangaDetails.tsx b/webUI/react/src/components/MangaDetails.tsx index 85f4c48..afc49f7 100644 --- a/webUI/react/src/components/MangaDetails.tsx +++ b/webUI/react/src/components/MangaDetails.tsx @@ -4,6 +4,7 @@ import { Button, createStyles, makeStyles } from '@material-ui/core'; import React, { useState } from 'react'; +import client from '../util/client'; import CategorySelect from './CategorySelect'; const useStyles = makeStyles(() => createStyles({ @@ -30,14 +31,14 @@ export default function MangaDetails(props: IProps) { function addToLibrary() { setInLibrary('adding'); - fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`).then(() => { + client.get(`/api/v1/manga/${manga.id}/library/`).then(() => { setInLibrary('In Library'); }); } function removeFromLibrary() { setInLibrary('removing'); - fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`, { method: 'DELETE', mode: 'cors' }).then(() => { + client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => { setInLibrary('Not In Library'); }); } diff --git a/webUI/react/src/screens/Extensions.tsx b/webUI/react/src/screens/Extensions.tsx index 7584450..a0fc243 100644 --- a/webUI/react/src/screens/Extensions.tsx +++ b/webUI/react/src/screens/Extensions.tsx @@ -5,6 +5,7 @@ import React, { useContext, useEffect, useState } from 'react'; import ExtensionCard from '../components/ExtensionCard'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; export default function Extensions() { const { setTitle } = useContext(NavBarTitle); @@ -12,8 +13,8 @@ export default function Extensions() { const [extensions, setExtensions] = useState([]); useEffect(() => { - fetch('http://127.0.0.1:4567/api/v1/extension/list') - .then((response) => response.json()) + client.get('/api/v1/extension/list') + .then((response) => response.data) .then((data) => setExtensions(data)); }, []); diff --git a/webUI/react/src/screens/Library.tsx b/webUI/react/src/screens/Library.tsx index c632dfc..5253d09 100644 --- a/webUI/react/src/screens/Library.tsx +++ b/webUI/react/src/screens/Library.tsx @@ -87,13 +87,14 @@ export default function Library() { ); }, []); + // console.log(client.defaults.baseURL); // fetch the current tab useEffect(() => { tabs.forEach((tab, index) => { if (tab.category.order === tabNum && !tab.isFetched) { // eslint-disable-next-line @typescript-eslint/no-shadow - fetch(`http://127.0.0.1:4567/api/v1/category/${tab.category.id}`) - .then((response) => response.json()) + client.get(`/api/v1/category/${tab.category.id}`) + .then((response) => response.data) .then((data: IManga[]) => { const tabsClone = JSON.parse(JSON.stringify(tabs)); tabsClone[index].mangas = data; @@ -122,7 +123,7 @@ export default function Library() { )); - // 160px is min-width for viewport width of >600 + // Visual Hack: 160px is min-width for viewport width of >600 const scrollableTabs = window.innerWidth < tabs.length * 160; toRender = ( <> diff --git a/webUI/react/src/screens/Manga.tsx b/webUI/react/src/screens/Manga.tsx index 5ba5d93..3c2bc20 100644 --- a/webUI/react/src/screens/Manga.tsx +++ b/webUI/react/src/screens/Manga.tsx @@ -7,6 +7,7 @@ import { useParams } from 'react-router-dom'; import ChapterCard from '../components/ChapterCard'; import MangaDetails from '../components/MangaDetails'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; export default function Manga() { const { id } = useParams<{id: string}>(); @@ -16,8 +17,8 @@ export default function Manga() { const [chapters, setChapters] = useState([]); useEffect(() => { - fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`) - .then((response) => response.json()) + client.get(`/api/v1/manga/${id}/`) + .then((response) => response.data) .then((data: IManga) => { setManga(data); setTitle(data.title); @@ -25,8 +26,8 @@ export default function Manga() { }, []); useEffect(() => { - fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/chapters`) - .then((response) => response.json()) + client.get(`/api/v1/manga/${id}/chapters`) + .then((response) => response.data) .then((data) => setChapters(data)); }, []); diff --git a/webUI/react/src/screens/Reader.tsx b/webUI/react/src/screens/Reader.tsx index 357b292..f31a2c0 100644 --- a/webUI/react/src/screens/Reader.tsx +++ b/webUI/react/src/screens/Reader.tsx @@ -5,6 +5,8 @@ import React, { useContext, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; +import useLocalStorage from '../util/useLocalStorage'; const style = { display: 'flex', @@ -17,14 +19,15 @@ const style = { const range = (n:number) => Array.from({ length: n }, (value, key) => key); export default function Reader() { + const [serverAddress] = useLocalStorage('serverBaseURL', ''); const { setTitle } = useContext(NavBarTitle); const [pageCount, setPageCount] = useState(-1); const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>(); useEffect(() => { - fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`) - .then((response) => response.json()) + client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`) + .then((response) => response.data) .then((data:IChapter) => { setTitle(data.name); setPageCount(data.pageCount); @@ -41,7 +44,7 @@ export default function Reader() { const mapped = range(pageCount).map((index) => (
- f + F
)); return ( diff --git a/webUI/react/src/screens/SearchSingle.tsx b/webUI/react/src/screens/SearchSingle.tsx index c91aa23..fef7f67 100644 --- a/webUI/react/src/screens/SearchSingle.tsx +++ b/webUI/react/src/screens/SearchSingle.tsx @@ -9,6 +9,7 @@ import Button from '@material-ui/core/Button'; import { useParams } from 'react-router-dom'; import MangaGrid from '../components/MangaGrid'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; const useStyles = makeStyles((theme) => ({ root: { @@ -33,8 +34,8 @@ export default function SearchSingle() { const textInput = React.createRef(); useEffect(() => { - fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`) - .then((response) => response.json()) + client.get(`/api/v1/source/${sourceId}`) + .then((response) => response.data) .then((data: { name: string }) => setTitle(`Search: ${data.name}`)); }, []); @@ -54,8 +55,8 @@ export default function SearchSingle() { useEffect(() => { if (searchTerm.length > 0) { - fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`) - .then((response) => response.json()) + client.get(`/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`) + .then((response) => response.data) .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { if (data.mangaList.length > 0) { setMangas([ diff --git a/webUI/react/src/screens/Settings.tsx b/webUI/react/src/screens/Settings.tsx index a5f44b6..d0fa4f4 100644 --- a/webUI/react/src/screens/Settings.tsx +++ b/webUI/react/src/screens/Settings.tsx @@ -2,16 +2,21 @@ * 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 } from 'react'; +import React, { useContext, useState } from 'react'; import List from '@material-ui/core/List'; -import ListItem, { ListItemProps } from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; import InboxIcon from '@material-ui/icons/Inbox'; import Brightness6Icon from '@material-ui/icons/Brightness6'; -import { ListItemSecondaryAction, Switch } from '@material-ui/core'; +import DnsIcon from '@material-ui/icons/Dns'; +import EditIcon from '@material-ui/icons/Edit'; +import { + Button, Dialog, DialogActions, DialogContent, + DialogContentText, IconButton, ListItemSecondaryAction, Switch, TextField, + ListItemIcon, ListItemText, +} from '@material-ui/core'; +import ListItem, { ListItemProps } from '@material-ui/core/ListItem'; import NavBarTitle from '../context/NavbarTitle'; import DarkTheme from '../context/DarkTheme'; +import useLocalStorage from '../util/useLocalStorage'; function ListItemLink(props: ListItemProps<'a', { button?: true }>) { // eslint-disable-next-line react/jsx-props-no-spreading @@ -22,9 +27,26 @@ export default function Settings() { const { setTitle } = useContext(NavBarTitle); setTitle('Settings'); const { darkTheme, setDarkTheme } = useContext(DarkTheme); + const [serverAddress, setServerAddress] = useLocalStorage('serverBaseURL', ''); + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogValue, setDialogValue] = useState(serverAddress); + + const handleDialogOpen = () => { + setDialogValue(serverAddress); + setDialogOpen(true); + }; + + const handleDialogCancel = () => { + setDialogOpen(false); + }; + + const handleDialogSubmit = () => { + setDialogOpen(false); + setServerAddress(dialogValue); + }; return ( -
+ <> @@ -45,7 +67,49 @@ export default function Settings() { /> + + + + + + + { + handleDialogOpen(); + }} + > + + + + + -
+ + + + + Enter new category name. + + setDialogValue(e.target.value)} + /> + + + + + + + ); } diff --git a/webUI/react/src/screens/SourceMangas.tsx b/webUI/react/src/screens/SourceMangas.tsx index 24d5c28..c4cc2ff 100644 --- a/webUI/react/src/screens/SourceMangas.tsx +++ b/webUI/react/src/screens/SourceMangas.tsx @@ -6,6 +6,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import MangaGrid from '../components/MangaGrid'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; export default function SourceMangas(props: { popular: boolean }) { const { sourceId } = useParams<{sourceId: string}>(); @@ -15,15 +16,15 @@ export default function SourceMangas(props: { popular: boolean }) { const [lastPageNum, setLastPageNum] = useState(1); useEffect(() => { - fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`) - .then((response) => response.json()) + client.get(`/api/v1/source/${sourceId}`) + .then((response) => response.data) .then((data: { name: string }) => setTitle(data.name)); }, []); useEffect(() => { const sourceType = props.popular ? 'popular' : 'latest'; - fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) - .then((response) => response.json()) + client.get(`/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) + .then((response) => response.data) .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { setMangas([ ...mangas, diff --git a/webUI/react/src/screens/Sources.tsx b/webUI/react/src/screens/Sources.tsx index c7b2658..3715049 100644 --- a/webUI/react/src/screens/Sources.tsx +++ b/webUI/react/src/screens/Sources.tsx @@ -5,6 +5,7 @@ import React, { useContext, useEffect, useState } from 'react'; import SourceCard from '../components/SourceCard'; import NavBarTitle from '../context/NavbarTitle'; +import client from '../util/client'; export default function Sources() { const { setTitle } = useContext(NavBarTitle); @@ -13,8 +14,8 @@ export default function Sources() { const [fetched, setFetched] = useState(false); useEffect(() => { - fetch('http://127.0.0.1:4567/api/v1/source/list') - .then((response) => response.json()) + client.get('/api/v1/source/list') + .then((response) => response.data) .then((data) => { setSources(data); setFetched(true); }); }, []); diff --git a/webUI/react/src/screens/settings/Categories.jsx b/webUI/react/src/screens/settings/Categories.jsx index ac36e53..18e6dfc 100644 --- a/webUI/react/src/screens/settings/Categories.jsx +++ b/webUI/react/src/screens/settings/Categories.jsx @@ -28,6 +28,7 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import NavBarTitle from '../../context/NavbarTitle'; +import client from '../../util/client'; const getItemStyle = (isDragging, draggableStyle, palette) => ({ // styles we need to apply on draggables @@ -43,7 +44,7 @@ export default function Categories() { setTitle('Categories'); const [categories, setCategories] = useState([]); const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category - const [dialogOpen, setDialogOpen] = React.useState(false); + const [dialogOpen, setDialogOpen] = useState(false); const [dialogValue, setDialogValue] = useState(''); const theme = useTheme(); @@ -52,8 +53,8 @@ export default function Categories() { useEffect(() => { if (!dialogOpen) { - fetch('http://127.0.0.1:4567/api/v1/category/') - .then((response) => response.json()) + client.get('/api/v1/category/') + .then((response) => response.data) .then((data) => setCategories(data)); } }, [updateTriggerHolder]); @@ -64,11 +65,8 @@ export default function Categories() { const formData = new FormData(); formData.append('from', from + 1); formData.append('to', to + 1); - fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, { - method: 'PATCH', - mode: 'cors', - body: formData, - }).finally(() => triggerUpdate()); + client.post(`/api/v1/category/${category.id}/reorder`, formData) + .finally(() => triggerUpdate()); // also move it in local state to avoid jarring moving behviour... const result = Array.from(list); @@ -90,48 +88,40 @@ export default function Categories() { )); }; - const handleDialogOpen = () => { - setDialogOpen(true); - }; - const resetDialog = () => { - setDialogOpen(false); setDialogValue(''); setCategoryToEdit(-1); }; - const handleDialogCancel = () => { + const handleDialogOpen = () => { resetDialog(); + setDialogOpen(true); + }; + + const handleDialogCancel = () => { + setDialogOpen(false); }; const handleDialogSubmit = () => { - resetDialog(); + setDialogOpen(false); const formData = new FormData(); formData.append('name', dialogValue); if (categoryToEdit === -1) { - fetch('http://127.0.0.1:4567/api/v1/category/', { - method: 'POST', - mode: 'cors', - body: formData, - }).finally(() => triggerUpdate()); + client.post('/api/v1/category/', formData) + .finally(() => triggerUpdate()); } else { const category = categories[categoryToEdit]; - fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, { - method: 'PATCH', - mode: 'cors', - body: formData, - }).finally(() => triggerUpdate()); + client.patch(`/api/v1/category/${category.id}`, formData) + .finally(() => triggerUpdate()); } }; const deleteCategory = (index) => { const category = categories[index]; - fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, { - method: 'DELETE', - mode: 'cors', - }).finally(() => triggerUpdate()); + client.delete(`/api/v1/category/${category.id}`) + .finally(() => triggerUpdate()); }; return ( @@ -167,8 +157,8 @@ export default function Categories() { /> { - setCategoryToEdit(index); handleDialogOpen(); + setCategoryToEdit(index); }} > @@ -201,7 +191,7 @@ export default function Categories() { > - + {categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`} diff --git a/webUI/react/src/util/client.tsx b/webUI/react/src/util/client.tsx index ce4884c..1f69400 100644 --- a/webUI/react/src/util/client.tsx +++ b/webUI/react/src/util/client.tsx @@ -1,10 +1,26 @@ -import axios from 'axios'; -import storage from './storage'; +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ -const clientMaker = () => axios.create({ - baseURL: storage.getItem('baseURL', 'http://127.0.0.1:4567'), +import axios from 'axios'; +import storage from './localStorage'; + +const { hostname, port, protocol } = window.location; + +// if port is 3000 it's probably running from webpack devlopment server +let inferredPort; +if (port === '3000') { inferredPort = '4567'; } else { inferredPort = port; } + +const client = axios.create({ + // baseURL must not have traling slash + baseURL: storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`), }); -const client = clientMaker(); +client.interceptors.request.use((config) => { + if (config.data instanceof FormData) { + Object.assign(config.headers, { 'Content-Type': 'multipart/form-data' }); + } + return config; +}); export default client; diff --git a/webUI/react/src/util/storage.tsx b/webUI/react/src/util/localStorage.tsx similarity index 51% rename from webUI/react/src/util/storage.tsx rename to webUI/react/src/util/localStorage.tsx index 2a6bf7c..bdb836b 100644 --- a/webUI/react/src/util/storage.tsx +++ b/webUI/react/src/util/localStorage.tsx @@ -1,14 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + function getItem(key: string, defaultValue: T) : T { try { const item = window.localStorage.getItem(key); - if (item !== null) { return JSON.parse(item); } + if (item !== null) { + return JSON.parse(item); + } window.localStorage.setItem(key, JSON.stringify(defaultValue)); - } finally { - // eslint-disable-next-line no-unsafe-finally - return defaultValue; - } + + /* eslint-disable no-empty */ + } finally { } + return defaultValue; } function setItem(key: string, value: T): void { @@ -16,7 +22,7 @@ function setItem(key: string, value: T): void { window.localStorage.setItem(key, JSON.stringify(value)); // eslint-disable-next-line no-empty - } catch (error) { } + } finally { } } export default { getItem, setItem }; diff --git a/webUI/react/src/util/useLocalStorage.tsx b/webUI/react/src/util/useLocalStorage.tsx index 6e3902f..d3df299 100644 --- a/webUI/react/src/util/useLocalStorage.tsx +++ b/webUI/react/src/util/useLocalStorage.tsx @@ -3,39 +3,17 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useState, Dispatch, SetStateAction } from 'react'; +import storage from './localStorage'; // eslint-disable-next-line max-len -export default function useLocalStorage(key: string, initialValue: T) : [T, Dispatch>] { - // State to store our value - // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = useState(() => { - try { - // Get from local storage by key - const item = window.localStorage.getItem(key); +export default function useLocalStorage(key: string, defaultValue: T) : [T, Dispatch>] { + const [storedValue, setStoredValue] = useState(storage.getItem(key, defaultValue)); - // Parse stored json or if null return set and return initialValue - if (item !== null) { return JSON.parse(item); } - - window.localStorage.setItem(key, JSON.stringify(initialValue)); - } finally { - // eslint-disable-next-line no-unsafe-finally - return initialValue; - } - }); - - // Return a wrapped version of useState's setter function that ... - // ... persists the new value to localStorage. const setValue = (value: T | ((prevState: T) => T)) => { - try { // Allow value to be a function so we have same API as useState - const valueToStore = value instanceof Function ? value(storedValue) : value; - // Save state - setStoredValue(valueToStore); - // Save to local storage - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - - // eslint-disable-next-line no-empty - } catch (error) { } + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + storage.setItem(key, valueToStore); }; return [storedValue, setValue];