Rewrite useQueryAndSortParams machinery to be faster/simpler/better (#758)

* Rewrite useQueryAndSortParams machinery to be faster/simpler/better

* Politely debounce Algolia querying

* Tidy some stuff up

* Style changes suggested by James
This commit is contained in:
Marshall Polaris 2022-08-14 22:09:25 -07:00 committed by GitHub
parent 5d14d79e6e
commit 972f215f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 150 deletions

View File

@ -4,11 +4,7 @@ import { SearchOptions } from '@algolia/client-search'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params'
QuerySortOptions,
Sort,
useQueryAndSortParams,
} from '../hooks/use-sort-and-query-params'
import { import {
ContractHighlightOptions, ContractHighlightOptions,
ContractsGrid, ContractsGrid,
@ -24,11 +20,25 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore'
import { useMemberGroups } from 'web/hooks/use-group' import { useMemberGroups } from 'web/hooks/use-group'
import { NEW_USER_GROUP_SLUGS } from 'common/group' import { NEW_USER_GROUP_SLUGS } from 'common/group'
import { PillButton } from './buttons/pill-button' import { PillButton } from './buttons/pill-button'
import { sortBy } from 'lodash' import { debounce, sortBy } from 'lodash'
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
import { Col } from './layout/col' import { Col } from './layout/col'
import { safeLocalStorage } from 'web/lib/util/local'
import clsx from 'clsx' import clsx from 'clsx'
// TODO: this obviously doesn't work with SSR, common sense would suggest
// that we should save things like this in cookies so the server has them
const MARKETS_SORT = 'markets_sort'
function setSavedSort(s: Sort) {
safeLocalStorage()?.setItem(MARKETS_SORT, s)
}
function getSavedSort() {
return safeLocalStorage()?.getItem(MARKETS_SORT) as Sort | null | undefined
}
const searchClient = algoliasearch( const searchClient = algoliasearch(
'GJQPAYENIF', 'GJQPAYENIF',
'75c28fc084a80e1129d427d470cf41a3' '75c28fc084a80e1129d427d470cf41a3'
@ -47,7 +57,6 @@ const sortOptions = [
{ label: 'Close date', value: 'close-date' }, { label: 'Close date', value: 'close-date' },
{ label: 'Resolve date', value: 'resolve-date' }, { label: 'Resolve date', value: 'resolve-date' },
] ]
export const DEFAULT_SORT = 'score'
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
@ -68,7 +77,8 @@ type AdditionalFilter = {
export function ContractSearch(props: { export function ContractSearch(props: {
user?: User | null user?: User | null
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions defaultSort?: Sort
defaultFilter?: filter
additionalFilter?: AdditionalFilter additionalFilter?: AdditionalFilter
highlightOptions?: ContractHighlightOptions highlightOptions?: ContractHighlightOptions
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
@ -79,10 +89,13 @@ export function ContractSearch(props: {
hideQuickBet?: boolean hideQuickBet?: boolean
} }
headerClassName?: string headerClassName?: string
useQuerySortLocalStorage?: boolean
useQuerySortUrlParams?: boolean
}) { }) {
const { const {
user, user,
querySortOptions, defaultSort,
defaultFilter,
additionalFilter, additionalFilter,
onContractClick, onContractClick,
overrideGridClassName, overrideGridClassName,
@ -90,6 +103,8 @@ export function ContractSearch(props: {
cardHideOptions, cardHideOptions,
highlightOptions, highlightOptions,
headerClassName, headerClassName,
useQuerySortLocalStorage,
useQuerySortUrlParams,
} = props } = props
const [numPages, setNumPages] = useState(1) const [numPages, setNumPages] = useState(1)
@ -132,31 +147,33 @@ export function ContractSearch(props: {
} }
} }
const onSearchParametersChanged = useRef(
debounce((params) => {
searchParameters.current = params
performQuery(true)
}, 100)
).current
const contracts = pages const contracts = pages
.flat() .flat()
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
return ( return <ContractSearchFirestore additionalFilter={additionalFilter} />
<ContractSearchFirestore
querySortOptions={querySortOptions}
additionalFilter={additionalFilter}
/>
)
} }
return ( return (
<Col className="h-full"> <Col className="h-full">
<ContractSearchControls <ContractSearchControls
className={headerClassName} className={headerClassName}
defaultSort={defaultSort}
defaultFilter={defaultFilter}
additionalFilter={additionalFilter} additionalFilter={additionalFilter}
hideOrderSelector={hideOrderSelector} hideOrderSelector={hideOrderSelector}
querySortOptions={querySortOptions} useQuerySortLocalStorage={useQuerySortLocalStorage}
useQuerySortUrlParams={useQuerySortUrlParams}
user={user} user={user}
onSearchParametersChanged={(params) => { onSearchParametersChanged={onSearchParametersChanged}
searchParameters.current = params
performQuery(true)
}}
/> />
<ContractsGrid <ContractsGrid
contracts={pages.length === 0 ? undefined : contracts} contracts={pages.length === 0 ? undefined : contracts}
@ -173,23 +190,40 @@ export function ContractSearch(props: {
function ContractSearchControls(props: { function ContractSearchControls(props: {
className?: string className?: string
defaultSort?: Sort
defaultFilter?: filter
additionalFilter?: AdditionalFilter additionalFilter?: AdditionalFilter
hideOrderSelector?: boolean hideOrderSelector?: boolean
onSearchParametersChanged: (params: SearchParameters) => void onSearchParametersChanged: (params: SearchParameters) => void
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions useQuerySortLocalStorage?: boolean
useQuerySortUrlParams?: boolean
user?: User | null user?: User | null
}) { }) {
const { const {
className, className,
defaultSort,
defaultFilter,
additionalFilter, additionalFilter,
hideOrderSelector, hideOrderSelector,
onSearchParametersChanged, onSearchParametersChanged,
querySortOptions, useQuerySortLocalStorage,
useQuerySortUrlParams,
user, user,
} = props } = props
const { query, setQuery, sort, setSort } = const savedSort = useQuerySortLocalStorage ? getSavedSort() : null
useQueryAndSortParams(querySortOptions) const initialSort = savedSort ?? defaultSort ?? 'score'
const querySortOpts = { useUrl: !!useQuerySortUrlParams }
const [sort, setSort] = useSort(initialSort, querySortOpts)
const [query, setQuery] = useQuery('', querySortOpts)
const [filter, setFilter] = useState<filter>(defaultFilter ?? 'open')
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
useEffect(() => {
if (useQuerySortLocalStorage) {
setSavedSort(sort)
}
}, [sort])
const follows = useFollows(user?.id) const follows = useFollows(user?.id)
const memberGroups = (useMemberGroups(user?.id) ?? []).filter( const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
@ -208,12 +242,6 @@ function ContractSearchControls(props: {
const pillGroups: { name: string; slug: string }[] = const pillGroups: { name: string; slug: string }[] =
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
const [filter, setFilter] = useState<filter>(
querySortOptions?.defaultFilter ?? 'open'
)
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
const additionalFilters = [ const additionalFilters = [
additionalFilter?.creatorId additionalFilter?.creatorId
? `creatorId:${additionalFilter.creatorId}` ? `creatorId:${additionalFilter.creatorId}`

View File

@ -106,11 +106,8 @@ export function CreatorContractsList(props: {
return ( return (
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort="newest"
defaultSort: 'newest', defaultFilter="all"
defaultFilter: 'all',
shouldLoadFromStorage: false,
}}
additionalFilter={{ additionalFilter={{
creatorId: creator.id, creatorId: creator.id,
}} }}

View File

@ -69,7 +69,6 @@ export function MarketModal(props: {
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
} }
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
querySortOptions={{ disableQueryString: true }}
highlightOptions={{ highlightOptions={{
contractIds: contracts.map((c) => c.id), contractIds: contracts.map((c) => c.id),
highlightClassName: highlightClassName:

View File

@ -1,9 +1,5 @@
import { debounce } from 'lodash' import { useState } from 'react'
import { useRouter } from 'next/router' import { NextRouter, useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { DEFAULT_SORT } from 'web/components/contract-search'
const MARKETS_SORT = 'markets_sort'
export type Sort = export type Sort =
| 'newest' | 'newest'
@ -15,92 +11,55 @@ export type Sort =
| 'last-updated' | 'last-updated'
| 'score' | 'score'
export function getSavedSort() { type UpdatedQueryParams = { [k: string]: string }
// TODO: this obviously doesn't work with SSR, common sense would suggest type QuerySortOpts = { useUrl: boolean }
// that we should save things like this in cookies so the server has them
if (typeof window !== 'undefined') { function withURLParams(location: Location, params: UpdatedQueryParams) {
return localStorage.getItem(MARKETS_SORT) as Sort | null const newParams = new URLSearchParams(location.search)
} else { for (const [k, v] of Object.entries(params)) {
return null if (!v) {
newParams.delete(k)
} else {
newParams.set(k, v)
}
} }
const newUrl = new URL(location.href)
newUrl.search = newParams.toString()
return newUrl
} }
export interface QuerySortOptions { function updateURL(params: UpdatedQueryParams) {
defaultSort?: Sort // see relevant discussion here https://github.com/vercel/next.js/discussions/18072
shouldLoadFromStorage?: boolean const url = withURLParams(window.location, params).toString()
/** Use normal react state instead of url query string */ const updatedState = { ...window.history.state, as: url, url }
disableQueryString?: boolean window.history.replaceState(updatedState, '', url)
} }
export function useQueryAndSortParams({ function getStringURLParam(router: NextRouter, k: string) {
defaultSort = DEFAULT_SORT, const v = router.query[k]
shouldLoadFromStorage = true, return typeof v === 'string' ? v : null
disableQueryString, }
}: QuerySortOptions = {}) {
export function useQuery(defaultQuery: string, opts?: QuerySortOpts) {
const useUrl = opts?.useUrl ?? false
const router = useRouter() const router = useRouter()
const initialQuery = useUrl ? getStringURLParam(router, 'q') : null
const { s: sort, q: query } = router.query as { const [query, setQuery] = useState(initialQuery ?? defaultQuery)
q?: string if (!useUrl) {
s?: Sort return [query, setQuery] as const
} } else {
return [query, (q: string) => (setQuery(q), updateURL({ q }))] as const
const setSort = (sort: Sort | undefined) => { }
router.replace({ query: { ...router.query, s: sort } }, undefined, { }
shallow: true,
}) export function useSort(defaultSort: Sort, opts?: QuerySortOpts) {
if (shouldLoadFromStorage) { const useUrl = opts?.useUrl ?? false
localStorage.setItem(MARKETS_SORT, sort || '') const router = useRouter()
} const initialSort = useUrl ? (getStringURLParam(router, 's') as Sort) : null
} const [sort, setSort] = useState(initialSort ?? defaultSort)
if (!useUrl) {
const [queryState, setQueryState] = useState(query) return [sort, setSort] as const
} else {
useEffect(() => { return [sort, (s: Sort) => (setSort(s), updateURL({ s }))] as const
setQueryState(query)
}, [query])
// Debounce router query update.
const pushQuery = useMemo(
() =>
debounce((query: string | undefined) => {
const queryObj = { ...router.query, q: query }
if (!query) delete queryObj.q
router.replace({ query: queryObj }, undefined, {
shallow: true,
})
}, 100),
[router]
)
const setQuery = (query: string | undefined) => {
setQueryState(query)
if (!disableQueryString) {
pushQuery(query)
}
}
useEffect(() => {
// If there's no sort option, then set the one from localstorage
if (router.isReady && !sort && shouldLoadFromStorage) {
const localSort = localStorage.getItem(MARKETS_SORT) as Sort
if (localSort && localSort !== defaultSort) {
// Use replace to not break navigating back.
router.replace(
{ query: { ...router.query, s: localSort } },
undefined,
{ shallow: true }
)
}
}
})
// use normal state if querydisableQueryString
const [sortState, setSortState] = useState(defaultSort)
return {
sort: disableQueryString ? sortState : sort ?? defaultSort,
query: queryState ?? '',
setSort: disableQueryString ? setSortState : setSort,
setQuery,
} }
} }

View File

@ -3,16 +3,11 @@ import { searchInAny } from 'common/util/parse'
import { sortBy } from 'lodash' import { sortBy } from 'lodash'
import { ContractsGrid } from 'web/components/contract/contracts-grid' import { ContractsGrid } from 'web/components/contract/contracts-grid'
import { useContracts } from 'web/hooks/use-contracts' import { useContracts } from 'web/hooks/use-contracts'
import { import { Sort, useQuery, useSort } from 'web/hooks/use-sort-and-query-params'
QuerySortOptions,
Sort,
useQueryAndSortParams,
} from 'web/hooks/use-sort-and-query-params'
const MAX_CONTRACTS_RENDERED = 100 const MAX_CONTRACTS_RENDERED = 100
export default function ContractSearchFirestore(props: { export default function ContractSearchFirestore(props: {
querySortOptions?: QuerySortOptions
additionalFilter?: { additionalFilter?: {
creatorId?: string creatorId?: string
tag?: string tag?: string
@ -21,10 +16,9 @@ export default function ContractSearchFirestore(props: {
} }
}) { }) {
const contracts = useContracts() const contracts = useContracts()
const { querySortOptions, additionalFilter } = props const { additionalFilter } = props
const [query, setQuery] = useQuery('', { useUrl: true })
const { query, setQuery, sort, setSort } = const [sort, setSort] = useSort('score', { useUrl: true })
useQueryAndSortParams(querySortOptions)
let matches = (contracts ?? []).filter((c) => let matches = (contracts ?? []).filter((c) =>
searchInAny( searchInAny(

View File

@ -31,7 +31,6 @@ import { CreateQuestionButton } from 'web/components/create-question-button'
import React, { useState } from 'react' import React, { useState } from 'react'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { getSavedSort } from 'web/hooks/use-sort-and-query-params'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useCommentsOnGroup } from 'web/hooks/use-comments' import { useCommentsOnGroup } from 'web/hooks/use-comments'
@ -196,11 +195,8 @@ export default function GroupPage(props: {
const questionsTab = ( const questionsTab = (
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort={'newest'}
shouldLoadFromStorage: true, defaultFilter={'open'}
defaultSort: getSavedSort() ?? 'newest',
defaultFilter: 'open',
}}
additionalFilter={{ groupSlug: group.slug }} additionalFilter={{ groupSlug: group.slug }}
/> />
) )

View File

@ -4,8 +4,7 @@ import { PlusSmIcon } from '@heroicons/react/solid'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ContractSearch } from 'web/components/contract-search'
import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { ContractPageContent } from './[username]/[contractSlug]' import { ContractPageContent } from './[username]/[contractSlug]'
@ -35,10 +34,8 @@ const Home = (props: { auth: { user: User } }) => {
<Col className="mx-auto w-full p-2"> <Col className="mx-auto w-full p-2">
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ useQuerySortLocalStorage={true}
shouldLoadFromStorage: true, useQuerySortUrlParams={true}
defaultSort: getSavedSort() ?? DEFAULT_SORT,
}}
onContractClick={(c) => { onContractClick={(c) => {
// Show contract without navigating to contract page. // Show contract without navigating to contract page.
setContract(c) setContract(c)

View File

@ -15,11 +15,8 @@ export default function TagPage() {
<Title text={`#${tag}`} /> <Title text={`#${tag}`} />
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort="newest"
defaultSort: 'newest', defaultFilter="all"
defaultFilter: 'all',
shouldLoadFromStorage: true,
}}
additionalFilter={{ tag }} additionalFilter={{ tag }}
/> />
</Page> </Page>