Skip to content
This repository has been archived by the owner on Jan 15, 2021. It is now read-only.

useDataFilter hook #1094

Merged
merged 10 commits into from
Jun 15, 2020
97 changes: 56 additions & 41 deletions src/components/DepositWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react'
import React, { useCallback, useMemo } from 'react'
import Modali from 'modali'
import styled from 'styled-components'
import BN from 'bn.js'
Expand All @@ -10,6 +10,8 @@ import searchIcon from 'assets/img/search.svg'
import { logDebug, getToken } from 'utils'
import { ZERO, MEDIA } from 'const'
import { TokenBalanceDetails } from 'types'
import { TokenLocalState } from 'reducers-actions'
import { LocalTokensState } from 'reducers-actions/localTokens'

// Components
import { CardTable } from 'components/Layout/Card'
Expand All @@ -25,11 +27,10 @@ import { useDepositModals } from 'components/DepositWidget/useDepositModals'
import { useTokenBalances } from 'hooks/useTokenBalances'
import useSafeState from 'hooks/useSafeState'
import useWindowSizes from 'hooks/useWindowSizes'
import { useDebounce } from 'hooks/useDebounce'
import { useManageTokens } from 'hooks/useManageTokens'
import useGlobalState from 'hooks/useGlobalState'
import { useEthBalances } from 'hooks/useEthBalance'
import { TokenLocalState } from 'reducers-actions'
import useDataFilter from 'hooks/useDataFilter'

interface WithdrawState {
amount: BN
Expand Down Expand Up @@ -335,6 +336,30 @@ interface BalanceDisplayProps extends TokenLocalState {
): Promise<void>
}

const customFilterFnFactory = (localTokens: LocalTokensState) => (searchTxt: string) => ({
symbol,
name,
address,
}: TokenBalanceDetails): boolean => {
if (localTokens.disabled.has(address)) return false

if (searchTxt === '') return true

return (
symbol?.toLowerCase().includes(searchTxt) ||
name?.toLowerCase().includes(searchTxt) ||
address.toLowerCase().includes(searchTxt)
)
}

const customHideZeroFilterFn = ({
totalExchangeBalance,
pendingWithdraw,
walletBalance,
}: TokenBalanceDetails): boolean => {
return !totalExchangeBalance.isZero() || !pendingWithdraw.amount.isZero() || !walletBalance.isZero()
}

const BalancesDisplay: React.FC<BalanceDisplayProps> = ({
ethBalance,
balances,
Expand All @@ -352,48 +377,38 @@ const BalancesDisplay: React.FC<BalanceDisplayProps> = ({
}) => {
const windowSpecs = useWindowSizes()

const [search, setSearch] = useState('')
const [hideZeroBalances, setHideZeroBalances] = useState(false)

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => setSearch(e.target.value)

const handleHideZeroBalances = (e: React.ChangeEvent<HTMLInputElement>): void => setHideZeroBalances(e.target.checked)

const { value: debouncedSearch, setImmediate: setDebouncedSearch } = useDebounce(search, 500)

const clearFilters = (): void => {
setSearch('')
setDebouncedSearch('')
setHideZeroBalances(false)
}

const [{ localTokens }] = useGlobalState()

const filteredBalances = useMemo(() => {
if ((!debouncedSearch && localTokens.disabled.size === 0) || !balances || balances.length === 0) return balances

const searchTxt = debouncedSearch.trim().toLowerCase()

return balances.filter(({ symbol, name, address }) => {
if (localTokens.disabled.has(address)) return false

if (searchTxt === '') return true

return (
symbol?.toLowerCase().includes(searchTxt) ||
name?.toLowerCase().includes(searchTxt) ||
address.toLowerCase().includes(searchTxt)
)
})
}, [debouncedSearch, balances, localTokens.disabled])
const memoizedSearchFilterParams = useMemo(
() => ({
data: balances,
filterFnFactory: customFilterFnFactory(localTokens),
userConditionalCheck: ({ debouncedSearch }: { debouncedSearch: string }): boolean =>
!debouncedSearch && localTokens.disabled.size === 0,
}),
[balances, localTokens],
)

const displayedBalances = useMemo(() => {
if (!hideZeroBalances || !filteredBalances || filteredBalances.length === 0) return filteredBalances
const {
filteredData: filteredBalances,
search,
handlers: { handleSearch },
} = useDataFilter(memoizedSearchFilterParams)

const memoizedZeroFilterParams = useMemo(
() => ({
data: filteredBalances,
isSearchFilter: false,
filterFnFactory: (): typeof customHideZeroFilterFn => customHideZeroFilterFn,
}),
[filteredBalances],
)

return filteredBalances.filter(({ totalExchangeBalance, pendingWithdraw, walletBalance }) => {
return !totalExchangeBalance.isZero() || !pendingWithdraw.amount.isZero() || !walletBalance.isZero()
})
}, [hideZeroBalances, filteredBalances])
const {
filteredData: displayedBalances,
showFilter: hideZeroBalances,
handlers: { handleToggleFilter: handleHideZeroBalances, clearFilters },
} = useDataFilter(memoizedZeroFilterParams)

const { modalProps, toggleModal } = useManageTokens()

Expand Down
48 changes: 24 additions & 24 deletions src/components/OrdersWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,17 @@ const OrdersWidget: React.FC = () => {
const { networkId, isConnected } = useWalletConnection()

// allOrders and markedForDeletion, split by tab
const [filteredOrders, setFilteredOrders] = useSafeState<FilteredOrdersState>(emptyState())
const [classifiedOrders, setClassifiedOrders] = useSafeState<FilteredOrdersState>(emptyState())
const [selectedTab, setSelectedTab] = useSafeState<OrderTabs>('active')

// syntactic sugar
const { displayedOrders, displayedPendingOrders, markedForDeletion } = useMemo(
() => ({
displayedOrders: filteredOrders[selectedTab].orders,
displayedPendingOrders: filteredOrders[selectedTab].pendingOrders,
markedForDeletion: filteredOrders[selectedTab].markedForDeletion,
displayedOrders: classifiedOrders[selectedTab].orders,
displayedPendingOrders: classifiedOrders[selectedTab].pendingOrders,
markedForDeletion: classifiedOrders[selectedTab].markedForDeletion,
}),
[filteredOrders, selectedTab],
[classifiedOrders, selectedTab],
)

const setSelectedTabFactory = useCallback(
Expand All @@ -123,22 +123,22 @@ const OrdersWidget: React.FC = () => {
[setSelectedTab],
)

// Update filteredOrders state whenever there's a change to allOrders
// Update classifiedOrders state whenever there's a change to allOrders
// splitting orders into respective tabs
useEffect(() => {
const filteredOrders = emptyState()
const classifiedOrders = emptyState()

classifyOrders(allOrders, filteredOrders, 'orders')
classifyOrders(allPendingOrders, filteredOrders, 'pendingOrders')
classifyOrders(allOrders, classifiedOrders, 'orders')
classifyOrders(allPendingOrders, classifiedOrders, 'pendingOrders')

setFilteredOrders(curr => {
setClassifiedOrders(curr => {
// copy markedForDeletion
Object.keys(filteredOrders).forEach(
type => (filteredOrders[type].markedForDeletion = curr[type].markedForDeletion),
Object.keys(classifiedOrders).forEach(
type => (classifiedOrders[type].markedForDeletion = curr[type].markedForDeletion),
)
return filteredOrders
return classifiedOrders
})
}, [allOrders, allPendingOrders, setFilteredOrders])
}, [allOrders, allPendingOrders, setClassifiedOrders])

const ordersCount = displayedOrders.length + displayedPendingOrders.length

Expand All @@ -161,7 +161,7 @@ const OrdersWidget: React.FC = () => {

const toggleMarkForDeletionFactory = useCallback(
(orderId: string, selectedTab: OrderTabs): (() => void) => (): void =>
setFilteredOrders(curr => {
setClassifiedOrders(curr => {
const state = emptyState()

// copy full state
Expand All @@ -176,24 +176,24 @@ const OrdersWidget: React.FC = () => {

return state
}),
[setFilteredOrders],
[setClassifiedOrders],
)

const toggleSelectAll = useCallback(
({ currentTarget: { checked } }: React.SyntheticEvent<HTMLInputElement>) =>
setFilteredOrders(curr => {
setClassifiedOrders(curr => {
const state = emptyState()

// copy full state
Object.keys(curr).forEach(tab => (state[tab] = curr[tab]))

state[selectedTab].markedForDeletion = checked
? new Set(filteredOrders[selectedTab].orders.map(order => order.id))
? new Set(classifiedOrders[selectedTab].orders.map(order => order.id))
: new Set()

return state
}),
[filteredOrders, selectedTab, setFilteredOrders],
[classifiedOrders, selectedTab, setClassifiedOrders],
)

const { deleteOrders, deleting } = useDeleteOrders()
Expand All @@ -208,7 +208,7 @@ const OrdersWidget: React.FC = () => {
unstable_batchedUpdates(() => {
// reset selections

setFilteredOrders(curr => {
setClassifiedOrders(curr => {
const state = emptyState()

// copy full state
Expand All @@ -228,7 +228,7 @@ const OrdersWidget: React.FC = () => {
})
}
},
[deleteOrders, forceOrdersRefresh, markedForDeletion, selectedTab, setFilteredOrders],
[deleteOrders, forceOrdersRefresh, markedForDeletion, selectedTab, setClassifiedOrders],
)

return (
Expand All @@ -250,19 +250,19 @@ const OrdersWidget: React.FC = () => {
<ShowOrdersButton
type="active"
isActive={selectedTab === 'active'}
count={filteredOrders.active.orders.length + filteredOrders.active.pendingOrders.length}
count={classifiedOrders.active.orders.length + classifiedOrders.active.pendingOrders.length}
onClick={setSelectedTabFactory('active')}
/>
<ShowOrdersButton
type="liquidity"
isActive={selectedTab === 'liquidity'}
count={filteredOrders.liquidity.orders.length + filteredOrders.liquidity.pendingOrders.length}
count={classifiedOrders.liquidity.orders.length + classifiedOrders.liquidity.pendingOrders.length}
onClick={setSelectedTabFactory('liquidity')}
/>
<ShowOrdersButton
type="closed"
isActive={selectedTab === 'closed'}
count={filteredOrders.closed.orders.length + filteredOrders.closed.pendingOrders.length}
count={classifiedOrders.closed.orders.length + classifiedOrders.closed.pendingOrders.length}
onClick={setSelectedTabFactory('closed')}
/>
</div>
Expand Down
78 changes: 78 additions & 0 deletions src/hooks/useDataFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useMemo, useState } from 'react'
import { useDebounce } from './useDebounce'

interface InternalState<T> {
debouncedSearch?: string
showFilter?: boolean
data?: T[]
}

interface HookResults<T> {
filteredData: T[]
search: string
showFilter: boolean
handlers: {
handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void
handleToggleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void
clearFilters: () => void
}
}

export interface HookParams<T> {
data: T[]
searchDebounceTime?: number
isSearchFilter?: boolean
userConditionalCheck?: (internalState?: InternalState<T>) => boolean
filterFnFactory: (searchTxt: string) => (params: T, index: number, array: T[]) => boolean
}

function useDataFilter<T>({
data,
isSearchFilter = true,
searchDebounceTime = 500,
filterFnFactory,
userConditionalCheck,
}: HookParams<T>): HookResults<T> {
const [search, setSearch] = useState('')
// searchFilter prop turns on/off by default. If using search filter, we dont want to use toggle
const [showFilter, setShowFilter] = useState(isSearchFilter)
const { value: debouncedSearch, setImmediate: setDebouncedSearch } = useDebounce(search, searchDebounceTime)

const handlers = useMemo(() => {
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => setSearch(e.target.value)
const handleToggleFilter = (): void => setShowFilter(state => !state)
const clearFilters = (): void => {
setSearch('')
setDebouncedSearch('')
setShowFilter(false)
}

return {
handleSearch,
handleToggleFilter,
clearFilters,
}
}, [setDebouncedSearch])

const filteredData = useMemo(() => {
const failsBasicSearchReq = !showFilter || (isSearchFilter && !debouncedSearch) || !data || data.length === 0
// pass internal state to user's userConditionalCheck fn for use outside
const failsUserReq = userConditionalCheck && userConditionalCheck({ debouncedSearch, data, showFilter })

if (failsBasicSearchReq || failsUserReq) return data

const searchTxt = debouncedSearch.trim().toLowerCase()
const customFilterFn = filterFnFactory(searchTxt)

return data.filter(customFilterFn)
}, [debouncedSearch, data, isSearchFilter, showFilter, userConditionalCheck, filterFnFactory])

return {
filteredData,
search,
showFilter,
handlers,
}
}

export default useDataFilter