diff --git a/src/components/Network.js b/src/components/Network.js index c32a54b..611f517 100644 --- a/src/components/Network.js +++ b/src/components/Network.js @@ -52,9 +52,7 @@ class Network extends React.Component { } loadPeers() { - const { - match: { params }, - } = this.props; + const { match: params } = this.props; networkApi .getPeerList() @@ -329,7 +327,7 @@ class Network extends React.Component { Select a peer to check its network status. - diff --git a/src/components/tx/Transactions.js b/src/components/tx/Transactions.js index 7edc612..a343fea 100644 --- a/src/components/tx/Transactions.js +++ b/src/components/tx/Transactions.js @@ -5,10 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import ReactLoading from 'react-loading'; -import { Link } from 'react-router-dom'; -import { isEqual } from 'lodash'; +import { Link, useLocation } from 'react-router-dom'; +import { reverse } from 'lodash'; import { TX_COUNT } from '../../constants'; import TxRow from './TxRow'; import helpers from '../../utils/helpers'; @@ -32,233 +32,263 @@ import PaginationURL from '../../utils/pagination'; * ts = "1579637190" * page = "next" */ -class Transactions extends React.Component { - constructor(props) { - super(props); +function Transactions({ shouldUpdateList, updateData, title }) { + // We can't use a simple variable here because it triggers a re-render everytime. + // useMemo was discussed but the idea is not to have a cache, it's more like + // a state without setter. + const [pagination] = useState( + () => + new PaginationURL({ + ts: { required: false }, + hash: { required: false }, + page: { required: false }, + }), + [] + ); - this.pagination = new PaginationURL({ - ts: { required: false }, - hash: { required: false }, - page: { required: false }, - }); - - this.state = { - transactions: [], - firstHash: null, - firstTimestamp: null, - lastHash: null, - lastTimestamp: null, - loaded: false, - hasAfter: false, - hasBefore: false, - queryParams: this.pagination.obtainQueryParams(), - }; - } + const location = useLocation(); - componentDidMount() { - this.getData(this.state.queryParams); + // loaded {boolean} Bool to show/hide loading element + const [loaded, setLoaded] = useState(false); + // transactions {Array} Transaction history + const [transactions, setTransactions] = useState([]); + // hasBefore {boolean} If 'Previous' button should be enabled + const [hasBefore, setHasBefore] = useState(false); + // hasAfter {boolean} If 'Next' button should be enabled + const [hasAfter, setHasAfter] = useState(false); + // firstHash {string | null} First hash of the current list + const [firstHash, setFirstHash] = useState(null); + // firstTimestamp {number | null} First timestamp of the current list + const [firstTimestamp, setFirstTimestamp] = useState(null); + // lastHash {string | null} Last hash of the current list + const [lastHash, setLastHash] = useState(null); + // lastTimestamp {number | null} Last timestamp of the current list + const [lastTimestamp, setLastTimestamp] = useState(null); - WebSocketHandler.on('network', this.handleWebsocket); - } + /** + * useCallback is important here to update this method with new history state + * otherwise it would be fixed the moment the event listener is started in the useEffect + * with the history as an empty array + * + * @param {Transaction} tx Transaction object that arrived from the websocket + */ + const updateListWs = useCallback( + tx => { + // We only add new tx/blocks if it's the first page + if (!hasBefore) { + if (shouldUpdateList(tx)) { + const newHasAfter = hasAfter || (transactions.length === TX_COUNT && !hasAfter); + const newTransactions = helpers.updateListWs(transactions, tx, TX_COUNT); - componentDidUpdate(prevProps, prevState) { - const queryParams = this.pagination.obtainQueryParams(); + const newFirstHash = transactions[0].tx_id; + const newFirstTimestamp = transactions[0].timestamp; + const newLastHash = transactions[transactions.length - 1].tx_id; + const newLastTimestamp = transactions[transactions.length - 1].timestamp; - // Do we have new URL params? - if (!isEqual(this.state.queryParams, queryParams)) { - // Fetch new data, unless query params were cleared and we were already in the most recent page - if (queryParams.hash || this.state.hasBefore) { - this.getData(queryParams); + // Finally we update the state again + setTransactions(newTransactions); + setFirstHash(newFirstHash); + setFirstTimestamp(newFirstTimestamp); + setLastHash(newLastHash); + setLastTimestamp(newLastTimestamp); + setHasAfter(newHasAfter); + } } - } - } - - componentWillUnmount() { - WebSocketHandler.removeListener('network', this.handleWebsocket); - } - - handleWebsocket = wsData => { - if (wsData.type === 'network:new_tx_accepted') { - this.updateListWs(wsData); - } - }; + }, + [transactions, hasAfter, hasBefore, shouldUpdateList] + ); - updateListWs = tx => { - // We only add new tx/blocks if it's the first page - if (!this.state.hasBefore) { - if (this.props.shouldUpdateList(tx)) { - let transactions = this.state.transactions; - let hasAfter = - this.state.hasAfter || (transactions.length === TX_COUNT && !this.state.hasAfter); - transactions = helpers.updateListWs(transactions, tx, TX_COUNT); - - let firstHash = transactions[0].tx_id; - let firstTimestamp = transactions[0].timestamp; - let lastHash = transactions[transactions.length - 1].tx_id; - let lastTimestamp = transactions[transactions.length - 1].timestamp; - - // Finally we update the state again - this.setState({ - transactions, - hasAfter, - firstHash, - lastHash, - firstTimestamp, - lastTimestamp, - }); + /** + * useCallback is needed here because this method is used as a dependency in the useEffect + * + * Method to handle websocket messages that arrive in the network scope + * This method will discard any messages that are not new transactions + * + * wsData {Object} Data send in the websocket message + */ + const handleWebsocket = useCallback( + wsData => { + if (wsData.type === 'network:new_tx_accepted') { + updateListWs(wsData); } - } - }; + }, + [updateListWs] + ); - handleDataFetched = (data, queryParams) => { - // Handle differently if is the first GET response we receive - // page indicates if was clicked 'previous' or 'next' - // Set first and last hash of the transactions - let firstHash = null; - let lastHash = null; - let firstTimestamp = null; - let lastTimestamp = null; - if (data.transactions.length) { - firstHash = data.transactions[0].tx_id; - lastHash = data.transactions[data.transactions.length - 1].tx_id; - firstTimestamp = data.transactions[0].timestamp; - lastTimestamp = data.transactions[data.transactions.length - 1].timestamp; - } + /** + * useCallback is needed here because this method is used as a dependency in another useCallback + * + * Method to handle state updates after the new data is fetched + * + * data {Object} Data object from POST response with array of transactions (data.transactions) + * queryParams {Object} Query parameters of the URL + */ + const handleDataFetched = useCallback( + (data, queryParams) => { + // Handle differently if is the first GET response we receive + // page indicates if was clicked 'previous' or 'next' + // Set first and last hash of the transactions - let hasAfter; - let hasBefore; - if (queryParams.page === 'previous') { - hasAfter = true; - hasBefore = data.has_more; - if (!hasBefore) { - // Went back to most recent page: clear URL params - this.pagination.clearOptionalQueryParams(); + if (queryParams.page === 'previous') { + // When we are querying the previous set of transactions + // the API return the oldest first, so we need to revert the history + reverse(data.transactions); } - } else if (queryParams.page === 'next') { - hasBefore = true; - hasAfter = data.has_more; - } else { - // First load without parameters - hasBefore = false; - hasAfter = data.has_more; - } - this.setState({ - transactions: data.transactions, - loaded: true, - firstHash, - lastHash, - firstTimestamp, - lastTimestamp, - hasAfter, - hasBefore, - queryParams, - }); - }; - - getData(queryParams) { - this.props.updateData(queryParams.ts, queryParams.hash, queryParams.page).then( - data => { - this.handleDataFetched(data, queryParams); - }, - e => { - // Error in request - console.log(e); + let newFirstHash = null; + let newLastHash = null; + let newFirstTimestamp = null; + let newLastTimestamp = null; + if (data.transactions.length) { + newFirstHash = data.transactions[0].tx_id; + newLastHash = data.transactions[data.transactions.length - 1].tx_id; + newFirstTimestamp = data.transactions[0].timestamp; + newLastTimestamp = data.transactions[data.transactions.length - 1].timestamp; } - ); - } - render() { - const loadPagination = () => { - if (this.state.transactions.length === 0) { - return null; + let newHasAfter; + let newHasBefore; + if (queryParams.page === 'previous') { + newHasAfter = true; + newHasBefore = data.has_more; + if (!newHasBefore) { + // Went back to most recent page: clear URL params + pagination.clearOptionalQueryParams(); + } + } else if (queryParams.page === 'next') { + newHasBefore = true; + newHasAfter = data.has_more; } else { - return ( - - ); + // First load without parameters + newHasBefore = false; + newHasAfter = data.has_more; } - }; - const loadTable = () => { - return ( -
- - - - - - - - - {loadTableBody()} -
HashTimestamp - Hash -
- Timestamp -
-
+ setTransactions(data.transactions); + setFirstHash(newFirstHash); + setFirstTimestamp(newFirstTimestamp); + setLastHash(newLastHash); + setLastTimestamp(newLastTimestamp); + setHasAfter(newHasAfter); + setHasBefore(newHasBefore); + setLoaded(true); + }, + [pagination] + ); + + /** + * useCallback is needed here because this method is used as a dependency in the useEffect + * + * Method used to call the props method that will fetch new data + * depending on the current query params + * + * queryParams {Object} Query parameters of the URL + */ + const getData = useCallback( + queryParams => { + updateData(queryParams.ts, queryParams.hash, queryParams.page).then( + data => { + handleDataFetched(data, queryParams); + }, + e => { + // Error in request + console.log(e); + } ); - }; + }, + [handleDataFetched, updateData] + ); - const loadTableBody = () => { - return this.state.transactions.map((tx, idx) => { - return ; - }); + useEffect(() => { + // Handle load history depending on the query params in the URL + getData(pagination.obtainQueryParams()); + }, [location, getData, pagination]); + + useEffect(() => { + // Handle new txs in the network to update the list in real time + WebSocketHandler.on('network', handleWebsocket); + + return () => { + WebSocketHandler.removeListener('network', handleWebsocket); }; + }, [handleWebsocket]); + const loadPagination = () => { + if (transactions.length === 0) { + return null; + } return ( -
- {this.props.title} - {!this.state.loaded ? ( - - ) : ( - loadTable() - )} - {loadPagination()} + + ); + }; + + const loadTable = () => { + return ( +
+ + + + + + + + + {loadTableBody()} +
HashTimestamp + Hash +
+ Timestamp +
); - } + }; + + const loadTableBody = () => { + return transactions.map(tx => { + return ; + }); + }; + + return ( +
+ {title} + {!loaded ? : loadTable()} + {loadPagination()} +
+ ); } export default Transactions; diff --git a/src/screens/TokenDetail.js b/src/screens/TokenDetail.js index b291ab7..b281892 100644 --- a/src/screens/TokenDetail.js +++ b/src/screens/TokenDetail.js @@ -79,11 +79,9 @@ function TokenDetail() { } setToken(oldToken => { return { - token: { - ...oldToken, - uid: id, - meta: data, - }, + ...oldToken, + uid: id, + meta: data, }; }); };