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:
Tyler Hallada 2022-08-27 17:01:16 -04:00
parent f99a9cf79b
commit 236b4c84ca
7 changed files with 160 additions and 127 deletions

View File

@ -75,7 +75,6 @@ const AddModDialog: React.FC<Props> = ({ counts }) => {
</button>
<button
onClick={() => {
console.log(`Adding mod ${selectedMod} ${selectedPlugin}`);
if (data)
dispatch(updateFetchedPlugin({ ...data, enabled: true }));
setDialogShown(false);

View File

@ -10,6 +10,7 @@ import styles from "../styles/Map.module.css";
import Sidebar from "./Sidebar";
import ToggleLayersControl from "./ToggleLayersControl";
import SearchBar from "./SearchBar";
import SearchProvider from "./SearchProvider";
import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api";
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
@ -841,61 +842,60 @@ const Map: React.FC = () => {
ref={mapWrapper}
>
<div ref={mapContainer} className={styles["map-container"]}>
<Sidebar
selectedCell={selectedCell}
clearSelectedCell={() => {
console.log("clearSelectedCell");
router.push({ query: {} });
}}
setSelectedCells={setSelectedCells}
counts={counts}
countsError={countsError}
open={sidebarOpen}
setOpen={setSidebarOpenWithResize}
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 {
<SearchProvider>
<Sidebar
selectedCell={selectedCell}
clearSelectedCell={() => router.push({ query: {} })}
setSelectedCells={setSelectedCells}
counts={counts}
countsError={countsError}
open={sidebarOpen}
setOpen={setSidebarOpenWithResize}
lastModified={cellsData && cellsData.lastModified}
onSelectFile={(selectedFile) => {
const { plugin, ...withoutPlugin } = router.query;
router.push({ query: { ...withoutPlugin } });
}
}}
/>
<ToggleLayersControl map={map} />
<SearchBar
counts={counts}
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
/>
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;
router.push({ query: { ...withoutPlugin } });
}
}}
/>
<ToggleLayersControl map={map} />
<SearchBar
counts={counts}
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>
</>

View File

@ -1,10 +1,9 @@
import { useCombobox } from "downshift";
import React, { useEffect, useState, useRef } from "react";
import MiniSearch, { SearchResult } from "minisearch";
import useSWRImmutable from "swr/immutable";
import React, { useContext, useState, useRef } from "react";
import { SearchResult } from "minisearch";
import { SearchContext } from "./SearchProvider";
import styles from "../styles/SearchBar.module.css";
import { jsonFetcher } from "../lib/api";
type Props = {
counts: Record<number, [number, number, number]> | null;
@ -21,26 +20,6 @@ interface Mod {
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> = ({
counts,
sidebarOpen,
@ -50,37 +29,8 @@ const SearchBar: React.FC<Props> = ({
fixed = false,
inputRef,
}) => {
const [rendered, setRendered] = useState(false);
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 { cellSearch, modSearch, loading, loadError } =
useContext(SearchContext);
const searchInput = useRef<HTMLInputElement | null>(null);
const [searchFocused, setSearchFocused] = useState<boolean>(false);
const [results, setResults] = useState<SearchResult[]>([]);
@ -100,13 +50,13 @@ const SearchBar: React.FC<Props> = ({
if (inputValue) {
let results: SearchResult[] = [];
if (
modSearch.current &&
modSearch &&
!/(^cell\s?-?\d+\s?,?\s?-?\d*$)|(^-?\d+\s?,\s?-?\d*$)/i.test(
inputValue
)
) {
results = results.concat(
modSearch.current.search(inputValue).sort((resultA, resultB) => {
modSearch.search(inputValue).sort((resultA, resultB) => {
if (counts) {
const countA = counts[resultA.id];
const countB = counts[resultB.id];
@ -153,12 +103,13 @@ const SearchBar: React.FC<Props> = ({
<input
{...getInputProps({
type: "text",
placeholder,
placeholder:
modSearch && !loading ? placeholder : "Search (loading...)",
onFocus: () => setSearchFocused(true),
onBlur: () => {
if (!isOpen) setSearchFocused(false);
},
disabled: !data,
disabled: !modSearch,
ref: (ref) => {
searchInput.current = ref;
if (inputRef) inputRef.current = ref;
@ -182,9 +133,9 @@ const SearchBar: React.FC<Props> = ({
{result.name}
</li>
))}
{error && (
{loadError && (
<div className={styles.error}>
Error loading mod search index: {error}.
Error loading mod search index: {loadError}.
</div>
)}
</ul>

View 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;

View File

@ -95,8 +95,6 @@ const Sidebar: React.FC<Props> = ({
};
const renderOpenSidebar = () => {
console.log("render sidebar");
console.log(router.query.plugin);
if (selectedCell) {
return (
<div
@ -159,7 +157,6 @@ const Sidebar: React.FC<Props> = ({
</div>
);
} else {
console.log("render base page");
return (
<div
className={styles.sidebar}

14
package-lock.json generated
View File

@ -14,7 +14,7 @@
"javascript-color-gradient": "^1.3.2",
"js-cookie": "^3.0.1",
"mapbox-gl": "^2.6.1",
"minisearch": "^3.2.0",
"minisearch": "^5.0.0",
"next": "12.1.1-canary.15",
"react": "17.0.2",
"react-dom": "17.0.2",
@ -2440,9 +2440,9 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"node_modules/minisearch": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-3.2.0.tgz",
"integrity": "sha512-Nq3o/a9mhvokHXKCS9zxAd0t1z/eSjdtmvfBfvGI2D0/Fx8xUjrOdpjqbU7DXRyH8obowhELR1+L+i3TV7Y21g=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-5.0.0.tgz",
"integrity": "sha512-VEwBhl8aFtc2UG2XmP7a4XaZxVfNhe7GvB2W/ZRGbLL3P3LbBhkoOezBWsMqG8Mr5VonqXAMRWth79XXKja1bQ=="
},
"node_modules/ms": {
"version": "2.1.2",
@ -5274,9 +5274,9 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minisearch": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-3.2.0.tgz",
"integrity": "sha512-Nq3o/a9mhvokHXKCS9zxAd0t1z/eSjdtmvfBfvGI2D0/Fx8xUjrOdpjqbU7DXRyH8obowhELR1+L+i3TV7Y21g=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-5.0.0.tgz",
"integrity": "sha512-VEwBhl8aFtc2UG2XmP7a4XaZxVfNhe7GvB2W/ZRGbLL3P3LbBhkoOezBWsMqG8Mr5VonqXAMRWth79XXKja1bQ=="
},
"ms": {
"version": "2.1.2",

View File

@ -17,7 +17,7 @@
"javascript-color-gradient": "^1.3.2",
"js-cookie": "^3.0.1",
"mapbox-gl": "^2.6.1",
"minisearch": "^3.2.0",
"minisearch": "^5.0.0",
"next": "12.1.1-canary.15",
"react": "17.0.2",
"react-dom": "17.0.2",