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:
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user