SearchProvider singleton, fix lag with addAllAsync
Load modSearch data asynchronously and only rebuild the search if the page is refreshed. Fixes the lag when returning to the base sidebar page from a data page.
This commit is contained in:
parent
f99a9cf79b
commit
236b4c84ca
@ -75,7 +75,6 @@ const AddModDialog: React.FC<Props> = ({ counts }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(`Adding mod ${selectedMod} ${selectedPlugin}`);
|
|
||||||
if (data)
|
if (data)
|
||||||
dispatch(updateFetchedPlugin({ ...data, enabled: true }));
|
dispatch(updateFetchedPlugin({ ...data, enabled: true }));
|
||||||
setDialogShown(false);
|
setDialogShown(false);
|
||||||
|
@ -10,6 +10,7 @@ import styles from "../styles/Map.module.css";
|
|||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import ToggleLayersControl from "./ToggleLayersControl";
|
import ToggleLayersControl from "./ToggleLayersControl";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
|
import SearchProvider from "./SearchProvider";
|
||||||
import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api";
|
import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api";
|
||||||
|
|
||||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
|
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
|
||||||
@ -841,61 +842,60 @@ const Map: React.FC = () => {
|
|||||||
ref={mapWrapper}
|
ref={mapWrapper}
|
||||||
>
|
>
|
||||||
<div ref={mapContainer} className={styles["map-container"]}>
|
<div ref={mapContainer} className={styles["map-container"]}>
|
||||||
<Sidebar
|
<SearchProvider>
|
||||||
selectedCell={selectedCell}
|
<Sidebar
|
||||||
clearSelectedCell={() => {
|
selectedCell={selectedCell}
|
||||||
console.log("clearSelectedCell");
|
clearSelectedCell={() => router.push({ query: {} })}
|
||||||
router.push({ query: {} });
|
setSelectedCells={setSelectedCells}
|
||||||
}}
|
counts={counts}
|
||||||
setSelectedCells={setSelectedCells}
|
countsError={countsError}
|
||||||
counts={counts}
|
open={sidebarOpen}
|
||||||
countsError={countsError}
|
setOpen={setSidebarOpenWithResize}
|
||||||
open={sidebarOpen}
|
lastModified={cellsData && cellsData.lastModified}
|
||||||
setOpen={setSidebarOpenWithResize}
|
onSelectFile={(selectedFile) => {
|
||||||
lastModified={cellsData && cellsData.lastModified}
|
|
||||||
onSelectFile={(selectedFile) => {
|
|
||||||
const { plugin, ...withoutPlugin } = router.query;
|
|
||||||
if (selectedFile) {
|
|
||||||
router.push({
|
|
||||||
query: { ...withoutPlugin, file: selectedFile },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { file, ...withoutFile } = withoutPlugin;
|
|
||||||
router.push({ query: { ...withoutFile } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSelectPlugin={(selectedPlugin) => {
|
|
||||||
if (selectedPlugin) {
|
|
||||||
router.push({
|
|
||||||
query: { ...router.query, plugin: selectedPlugin },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { plugin, ...withoutPlugin } = router.query;
|
const { plugin, ...withoutPlugin } = router.query;
|
||||||
router.push({ query: { ...withoutPlugin } });
|
if (selectedFile) {
|
||||||
}
|
router.push({
|
||||||
}}
|
query: { ...withoutPlugin, file: selectedFile },
|
||||||
/>
|
});
|
||||||
<ToggleLayersControl map={map} />
|
} else {
|
||||||
<SearchBar
|
const { file, ...withoutFile } = withoutPlugin;
|
||||||
counts={counts}
|
router.push({ query: { ...withoutFile } });
|
||||||
sidebarOpen={sidebarOpen}
|
}
|
||||||
placeholder="Search mods or cells…"
|
}}
|
||||||
onSelectResult={(selectedItem) => {
|
onSelectPlugin={(selectedPlugin) => {
|
||||||
if (!selectedItem) return;
|
if (selectedPlugin) {
|
||||||
if (
|
router.push({
|
||||||
selectedItem.x !== undefined &&
|
query: { ...router.query, plugin: selectedPlugin },
|
||||||
selectedItem.y !== undefined
|
});
|
||||||
) {
|
} else {
|
||||||
router.push({
|
const { plugin, ...withoutPlugin } = router.query;
|
||||||
query: { cell: `${selectedItem.x},${selectedItem.y}` },
|
router.push({ query: { ...withoutPlugin } });
|
||||||
});
|
}
|
||||||
} else {
|
}}
|
||||||
router.push({ query: { mod: selectedItem.id } });
|
/>
|
||||||
}
|
<ToggleLayersControl map={map} />
|
||||||
}}
|
<SearchBar
|
||||||
includeCells
|
counts={counts}
|
||||||
fixed
|
sidebarOpen={sidebarOpen}
|
||||||
/>
|
placeholder="Search mods or cells…"
|
||||||
|
onSelectResult={(selectedItem) => {
|
||||||
|
if (!selectedItem) return;
|
||||||
|
if (
|
||||||
|
selectedItem.x !== undefined &&
|
||||||
|
selectedItem.y !== undefined
|
||||||
|
) {
|
||||||
|
router.push({
|
||||||
|
query: { cell: `${selectedItem.x},${selectedItem.y}` },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push({ query: { mod: selectedItem.id } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
includeCells
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</SearchProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { useCombobox } from "downshift";
|
import { useCombobox } from "downshift";
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useContext, useState, useRef } from "react";
|
||||||
import MiniSearch, { SearchResult } from "minisearch";
|
import { SearchResult } from "minisearch";
|
||||||
import useSWRImmutable from "swr/immutable";
|
|
||||||
|
|
||||||
|
import { SearchContext } from "./SearchProvider";
|
||||||
import styles from "../styles/SearchBar.module.css";
|
import styles from "../styles/SearchBar.module.css";
|
||||||
import { jsonFetcher } from "../lib/api";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
counts: Record<number, [number, number, number]> | null;
|
counts: Record<number, [number, number, number]> | null;
|
||||||
@ -21,26 +20,6 @@ interface Mod {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cells = [];
|
|
||||||
|
|
||||||
for (let x = -77; x < 76; x++) {
|
|
||||||
for (let y = -50; y < 45; y++) {
|
|
||||||
const id = `${x},${y}`;
|
|
||||||
cells.push({ id, name: `Cell ${id}`, x, y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cellSearch = new MiniSearch({
|
|
||||||
fields: ["id"],
|
|
||||||
storeFields: ["id", "name", "x", "y"],
|
|
||||||
tokenize: (s) => [s.replace(/(cell\s?)|\s/gi, "")],
|
|
||||||
searchOptions: {
|
|
||||||
fields: ["id"],
|
|
||||||
prefix: true,
|
|
||||||
fuzzy: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
cellSearch.addAll(cells);
|
|
||||||
|
|
||||||
const SearchBar: React.FC<Props> = ({
|
const SearchBar: React.FC<Props> = ({
|
||||||
counts,
|
counts,
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
@ -50,37 +29,8 @@ const SearchBar: React.FC<Props> = ({
|
|||||||
fixed = false,
|
fixed = false,
|
||||||
inputRef,
|
inputRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [rendered, setRendered] = useState(false);
|
const { cellSearch, modSearch, loading, loadError } =
|
||||||
|
useContext(SearchContext);
|
||||||
const modSearch = useRef<MiniSearch<Mod> | null>(
|
|
||||||
null
|
|
||||||
) as React.MutableRefObject<MiniSearch<Mod>>;
|
|
||||||
|
|
||||||
const { data, error } = useSWRImmutable(
|
|
||||||
rendered && `https://mods.modmapper.com/mod_search_index.json`,
|
|
||||||
(_) => jsonFetcher<Mod[]>(_)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// awful hack to delay rendering of the mod_search_index.json, since it can block rendering somehow
|
|
||||||
requestAnimationFrame(() => setRendered(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && !modSearch.current) {
|
|
||||||
modSearch.current = new MiniSearch({
|
|
||||||
fields: ["name"],
|
|
||||||
storeFields: ["name", "id"],
|
|
||||||
searchOptions: {
|
|
||||||
fields: ["name"],
|
|
||||||
fuzzy: 0.2,
|
|
||||||
prefix: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
modSearch.current.addAll(data);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const searchInput = useRef<HTMLInputElement | null>(null);
|
const searchInput = useRef<HTMLInputElement | null>(null);
|
||||||
const [searchFocused, setSearchFocused] = useState<boolean>(false);
|
const [searchFocused, setSearchFocused] = useState<boolean>(false);
|
||||||
const [results, setResults] = useState<SearchResult[]>([]);
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
@ -100,13 +50,13 @@ const SearchBar: React.FC<Props> = ({
|
|||||||
if (inputValue) {
|
if (inputValue) {
|
||||||
let results: SearchResult[] = [];
|
let results: SearchResult[] = [];
|
||||||
if (
|
if (
|
||||||
modSearch.current &&
|
modSearch &&
|
||||||
!/(^cell\s?-?\d+\s?,?\s?-?\d*$)|(^-?\d+\s?,\s?-?\d*$)/i.test(
|
!/(^cell\s?-?\d+\s?,?\s?-?\d*$)|(^-?\d+\s?,\s?-?\d*$)/i.test(
|
||||||
inputValue
|
inputValue
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
results = results.concat(
|
results = results.concat(
|
||||||
modSearch.current.search(inputValue).sort((resultA, resultB) => {
|
modSearch.search(inputValue).sort((resultA, resultB) => {
|
||||||
if (counts) {
|
if (counts) {
|
||||||
const countA = counts[resultA.id];
|
const countA = counts[resultA.id];
|
||||||
const countB = counts[resultB.id];
|
const countB = counts[resultB.id];
|
||||||
@ -153,12 +103,13 @@ const SearchBar: React.FC<Props> = ({
|
|||||||
<input
|
<input
|
||||||
{...getInputProps({
|
{...getInputProps({
|
||||||
type: "text",
|
type: "text",
|
||||||
placeholder,
|
placeholder:
|
||||||
|
modSearch && !loading ? placeholder : "Search (loading...)",
|
||||||
onFocus: () => setSearchFocused(true),
|
onFocus: () => setSearchFocused(true),
|
||||||
onBlur: () => {
|
onBlur: () => {
|
||||||
if (!isOpen) setSearchFocused(false);
|
if (!isOpen) setSearchFocused(false);
|
||||||
},
|
},
|
||||||
disabled: !data,
|
disabled: !modSearch,
|
||||||
ref: (ref) => {
|
ref: (ref) => {
|
||||||
searchInput.current = ref;
|
searchInput.current = ref;
|
||||||
if (inputRef) inputRef.current = ref;
|
if (inputRef) inputRef.current = ref;
|
||||||
@ -182,9 +133,9 @@ const SearchBar: React.FC<Props> = ({
|
|||||||
{result.name}
|
{result.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{error && (
|
{loadError && (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
Error loading mod search index: {error}.
|
Error loading mod search index: {loadError}.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
86
components/SearchProvider.tsx
Normal file
86
components/SearchProvider.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { createContext, useEffect, useRef, useState } from "react";
|
||||||
|
import MiniSearch from "minisearch";
|
||||||
|
import useSWRImmutable from "swr/immutable";
|
||||||
|
|
||||||
|
import { jsonFetcher } from "../lib/api";
|
||||||
|
|
||||||
|
interface Mod {
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cells = [];
|
||||||
|
|
||||||
|
for (let x = -77; x < 76; x++) {
|
||||||
|
for (let y = -50; y < 45; y++) {
|
||||||
|
const id = `${x},${y}`;
|
||||||
|
cells.push({ id, name: `Cell ${id}`, x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cellSearch = new MiniSearch({
|
||||||
|
fields: ["id"],
|
||||||
|
storeFields: ["id", "name", "x", "y"],
|
||||||
|
tokenize: (s) => [s.replace(/(cell\s?)|\s/gi, "")],
|
||||||
|
searchOptions: {
|
||||||
|
fields: ["id"],
|
||||||
|
prefix: true,
|
||||||
|
fuzzy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cellSearch.addAll(cells);
|
||||||
|
|
||||||
|
type SearchContext = {
|
||||||
|
cellSearch: MiniSearch;
|
||||||
|
modSearch?: MiniSearch;
|
||||||
|
loading: boolean;
|
||||||
|
loadError?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchContext = createContext<SearchContext>({
|
||||||
|
cellSearch,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SearchProvider: React.FC = ({ children }) => {
|
||||||
|
const modSearch = useRef<MiniSearch<Mod> | null>(
|
||||||
|
null
|
||||||
|
) as React.MutableRefObject<MiniSearch<Mod>>;
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const { data, error } = useSWRImmutable(
|
||||||
|
`https://mods.modmapper.com/mod_search_index.json`,
|
||||||
|
(_) => jsonFetcher<Mod[]>(_)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && !modSearch.current) {
|
||||||
|
modSearch.current = new MiniSearch({
|
||||||
|
fields: ["name"],
|
||||||
|
storeFields: ["name", "id"],
|
||||||
|
searchOptions: {
|
||||||
|
fields: ["name"],
|
||||||
|
fuzzy: 0.2,
|
||||||
|
prefix: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
modSearch.current.addAllAsync(data).then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchContext.Provider
|
||||||
|
value={{
|
||||||
|
modSearch: modSearch.current,
|
||||||
|
cellSearch,
|
||||||
|
loading,
|
||||||
|
loadError: error,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SearchContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchProvider;
|
@ -95,8 +95,6 @@ const Sidebar: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderOpenSidebar = () => {
|
const renderOpenSidebar = () => {
|
||||||
console.log("render sidebar");
|
|
||||||
console.log(router.query.plugin);
|
|
||||||
if (selectedCell) {
|
if (selectedCell) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -159,7 +157,6 @@ const Sidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("render base page");
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.sidebar}
|
className={styles.sidebar}
|
||||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"javascript-color-gradient": "^1.3.2",
|
"javascript-color-gradient": "^1.3.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"mapbox-gl": "^2.6.1",
|
"mapbox-gl": "^2.6.1",
|
||||||
"minisearch": "^3.2.0",
|
"minisearch": "^5.0.0",
|
||||||
"next": "12.1.1-canary.15",
|
"next": "12.1.1-canary.15",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
@ -2440,9 +2440,9 @@
|
|||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||||
},
|
},
|
||||||
"node_modules/minisearch": {
|
"node_modules/minisearch": {
|
||||||
"version": "3.2.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-5.0.0.tgz",
|
||||||
"integrity": "sha512-Nq3o/a9mhvokHXKCS9zxAd0t1z/eSjdtmvfBfvGI2D0/Fx8xUjrOdpjqbU7DXRyH8obowhELR1+L+i3TV7Y21g=="
|
"integrity": "sha512-VEwBhl8aFtc2UG2XmP7a4XaZxVfNhe7GvB2W/ZRGbLL3P3LbBhkoOezBWsMqG8Mr5VonqXAMRWth79XXKja1bQ=="
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -5274,9 +5274,9 @@
|
|||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||||
},
|
},
|
||||||
"minisearch": {
|
"minisearch": {
|
||||||
"version": "3.2.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-5.0.0.tgz",
|
||||||
"integrity": "sha512-Nq3o/a9mhvokHXKCS9zxAd0t1z/eSjdtmvfBfvGI2D0/Fx8xUjrOdpjqbU7DXRyH8obowhELR1+L+i3TV7Y21g=="
|
"integrity": "sha512-VEwBhl8aFtc2UG2XmP7a4XaZxVfNhe7GvB2W/ZRGbLL3P3LbBhkoOezBWsMqG8Mr5VonqXAMRWth79XXKja1bQ=="
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"javascript-color-gradient": "^1.3.2",
|
"javascript-color-gradient": "^1.3.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"mapbox-gl": "^2.6.1",
|
"mapbox-gl": "^2.6.1",
|
||||||
"minisearch": "^3.2.0",
|
"minisearch": "^5.0.0",
|
||||||
"next": "12.1.1-canary.15",
|
"next": "12.1.1-canary.15",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user