1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00

refactor token list (#8726)

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Brad Decker 2020-06-04 13:13:11 -05:00 committed by GitHub
parent b96eb55c76
commit ab06595a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 170 deletions

View File

@ -1 +1 @@
export { default } from './token-list.container'
export { default } from './token-list'

View File

@ -1,148 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from '@metamask/eth-token-tracker'
import { isEqual } from 'lodash'
import contracts from 'eth-contract-metadata'
import { I18nContext } from '../../../contexts/i18n'
import TokenCell from '../token-cell'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
class TokenList extends Component {
static contextType = I18nContext
static propTypes = {
assetImages: PropTypes.object.isRequired,
network: PropTypes.string.isRequired,
onTokenClick: PropTypes.func.isRequired,
tokens: PropTypes.array.isRequired,
userAddress: PropTypes.string.isRequired,
}
constructor () {
super()
this.state = {
error: null,
tokensLoading: false,
tokensWithBalances: [],
}
}
constructTokenTracker () {
const { network, tokens, userAddress } = this.props
if (!tokens || !tokens.length) {
this.setState({
tokensLoading: false,
tokensWithBalances: [],
})
return
}
this.setState({ tokensLoading: true })
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
return
}
const updateBalances = (tokensWithBalances) => {
this.setState({
error: null,
tokensLoading: false,
tokensWithBalances,
})
}
const showError = (error) => {
this.setState({
error,
tokensLoading: false,
})
}
this.tokenTracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: tokens,
pollingInterval: 8000,
})
this.tokenTracker.on('update', updateBalances)
this.tokenTracker.on('error', showError)
this.tokenTracker.updateBalances()
}
stopTokenTracker () {
if (this.tokenTracker) {
this.tokenTracker.stop()
this.tokenTracker.removeAllListeners('update')
this.tokenTracker.removeAllListeners('error')
}
}
componentDidMount () {
this.constructTokenTracker()
}
componentDidUpdate (prevProps) {
const { network, tokens, userAddress } = this.props
if (
isEqual(tokens, prevProps.tokens) &&
userAddress === prevProps.userAddress &&
network === prevProps.network
) {
return
}
this.stopTokenTracker()
this.constructTokenTracker()
}
componentWillUnmount () {
this.stopTokenTracker()
}
render () {
const t = this.context
const { error, tokensLoading, tokensWithBalances } = this.state
const { assetImages, network, onTokenClick } = this.props
if (network === 'loading' || tokensLoading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{t('loadingTokens')}
</div>
)
}
return (
<div>
{tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell
key={index}
{...tokenData}
outdatedBalance={Boolean(error)}
onClick={onTokenClick}
/>
)
})}
</div>
)
}
}
export default TokenList

View File

@ -1,21 +0,0 @@
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getSelectedAddress } from '../../../selectors'
import TokenList from './token-list.component'
function mapStateToProps (state) {
return {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: getSelectedAddress(state),
assetImages: state.metamask.assetImages,
}
}
const TokenListContainer = connect(mapStateToProps)(TokenList)
TokenListContainer.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}
export default TokenListContainer

View File

@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import contracts from 'eth-contract-metadata'
import { isEqual } from 'lodash'
import TokenCell from '../token-cell'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useTokenTracker } from '../../../hooks/useTokenTracker'
import { useSelector } from 'react-redux'
import { getAssetImages } from '../../../selectors'
import { getTokens } from '../../../ducks/metamask/metamask'
const defaultTokens = []
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
export default function TokenList ({ onTokenClick }) {
const t = useI18nContext()
const assetImages = useSelector(getAssetImages)
// use `isEqual` comparison function because the token array is serialized
// from the background so it has a new reference with each background update,
// even if the tokens haven't changed
const tokens = useSelector(getTokens, isEqual)
const { loading, error, tokensWithBalances } = useTokenTracker(tokens)
if (loading) {
return (
<div
style={{
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
}}
>
{t('loadingTokens')}
</div>
)
}
return (
<div>
{tokensWithBalances.map((tokenData, index) => {
tokenData.image = assetImages[tokenData.address]
return (
<TokenCell
key={index}
{...tokenData}
outdatedBalance={Boolean(error)}
onClick={onTokenClick}
/>
)
})}
</div>
)
}
TokenList.propTypes = {
onTokenClick: PropTypes.func.isRequired,
}

View File

@ -0,0 +1,88 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import TokenTracker from '@metamask/eth-token-tracker'
import { useSelector } from 'react-redux'
import { getCurrentNetwork, getSelectedAddress } from '../selectors'
export function useTokenTracker (tokens) {
const network = useSelector(getCurrentNetwork)
const userAddress = useSelector(getSelectedAddress)
const [loading, setLoading] = useState(() => tokens?.length >= 0)
const [tokensWithBalances, setTokensWithBalances] = useState([])
const [error, setError] = useState(null)
const tokenTracker = useRef(null)
const updateBalances = useCallback((tokensWithBalances) => {
setTokensWithBalances(tokensWithBalances)
setLoading(false)
setError(null)
}, [])
const showError = useCallback((error) => {
setError(error)
setLoading(false)
}, [])
const teardownTracker = useCallback(() => {
if (tokenTracker.current) {
tokenTracker.current.stop()
tokenTracker.current.removeAllListeners('update')
tokenTracker.current.removeAllListeners('error')
tokenTracker.current = null
}
}, [])
const buildTracker = useCallback((address, tokenList) => {
// clear out previous tracker, if it exists.
teardownTracker()
tokenTracker.current = new TokenTracker({
userAddress: address,
provider: global.ethereumProvider,
tokens: tokenList,
pollingInterval: 8000,
})
tokenTracker.current.on('update', updateBalances)
tokenTracker.current.on('error', showError)
tokenTracker.current.updateBalances()
}, [updateBalances, showError, teardownTracker])
// Effect to remove the tracker when the component is removed from DOM
// Do not overload this effect with additional dependencies. teardownTracker
// is the only dependency here, which itself has no dependencies and will
// never update. The lack of dependencies that change is what confirms
// that this effect only runs on mount/unmount
useEffect(() => {
return teardownTracker
}, [teardownTracker])
// Effect to set loading state and initialize tracker when values change
useEffect(() => {
// This effect will only run initially and when:
// 1. network is updated,
// 2. userAddress is changed,
// 3. token list is updated and not equal to previous list
// in any of these scenarios, we should indicate to the user that their token
// values are in the process of updating by setting loading state.
setLoading(true)
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
// If we do not have enough information to build a TokenTracker, we exit early
// When the values above change, the effect will be restarted. We also teardown
// tracker because inevitably this effect will run again momentarily.
teardownTracker()
return
}
if (tokens.length === 0) {
// sets loading state to false and token list to empty
updateBalances([])
}
buildTracker(userAddress, tokens)
}, [userAddress, network, tokens, updateBalances, buildTracker])
return { loading, tokensWithBalances, error }
}