Performance Improvements (Pure components, debounced gas slider) (#899)

* PureComponent a ton of non-connected components.

* Debounce gas price slider. Keep gas price in state to reflect changes immediately.

* PureComponent balance sidebar and swap unconnected components.

* Import correct component.

* Move debouncing of gas slider to sagas via gasPriceInputIntent action.

* Remove console log.

* Remove leftover file from merge.
This commit is contained in:
William O'Beirne 2018-01-29 14:13:46 -05:00 committed by Daniel Ternyak
parent ae8b123cfd
commit d1a2c885a2
53 changed files with 177 additions and 107 deletions

View File

@ -6,6 +6,7 @@ import {
SetValueFieldAction,
InputGasLimitAction,
InputGasPriceAction,
InputGasPriceIntentAction,
InputDataAction,
InputNonceAction,
ResetAction,
@ -25,6 +26,12 @@ const inputGasPrice = (payload: InputGasPriceAction['payload']) => ({
payload
});
type TInputGasPriceIntent = typeof inputGasPrice;
const inputGasPriceIntent = (payload: InputGasPriceIntentAction['payload']) => ({
type: TypeKeys.GAS_PRICE_INPUT_INTENT,
payload
});
type TInputNonce = typeof inputNonce;
const inputNonce = (payload: InputNonceAction['payload']) => ({
type: TypeKeys.NONCE_INPUT,
@ -79,6 +86,7 @@ const reset = (): ResetAction => ({ type: TypeKeys.RESET });
export {
TInputGasLimit,
TInputGasPrice,
TInputGasPriceIntent,
TInputNonce,
TInputData,
TSetGasLimitField,
@ -90,6 +98,7 @@ export {
TReset,
inputGasLimit,
inputGasPrice,
inputGasPriceIntent,
inputNonce,
inputData,
setGasLimitField,

View File

@ -10,6 +10,10 @@ interface InputGasPriceAction {
type: TypeKeys.GAS_PRICE_INPUT;
payload: string;
}
interface InputGasPriceIntentAction {
type: TypeKeys.GAS_PRICE_INPUT_INTENT;
payload: string;
}
interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT;
payload: string;
@ -84,6 +88,7 @@ type FieldAction =
export {
InputGasLimitAction,
InputGasPriceAction,
InputGasPriceIntentAction,
InputDataAction,
InputNonceAction,
SetGasLimitFieldAction,

View File

@ -30,6 +30,7 @@ export enum TypeKeys {
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
GAS_PRICE_INPUT_INTENT = 'GAS_PRICE_INPUT_INTENT',
NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET',

View File

@ -7,7 +7,7 @@ interface State {
isFading: boolean;
hasAcknowledged: boolean;
}
export default class AlphaAgreement extends React.Component<{}, State> {
export default class AlphaAgreement extends React.PureComponent<{}, State> {
public state = {
hasAcknowledged: !!localStorage.getItem(LS_KEY),
isFading: false

View File

@ -23,7 +23,7 @@ interface State {
decimal: string;
}
export default class AddCustomTokenForm extends React.Component<Props, State> {
export default class AddCustomTokenForm extends React.PureComponent<Props, State> {
public state: State = {
tokenSymbolLookup: {},
address: '',

View File

@ -19,7 +19,7 @@ interface State {
trackedTokens: { [symbol: string]: boolean };
showCustomTokenForm: boolean;
}
export default class TokenBalances extends React.Component<Props, State> {
export default class TokenBalances extends React.PureComponent<Props, State> {
public state = {
trackedTokens: {},
showCustomTokenForm: false

View File

@ -19,7 +19,7 @@ interface State {
showLongBalance: boolean;
}
export default class TokenRow extends React.Component<Props, State> {
export default class TokenRow extends React.PureComponent<Props, State> {
public state = {
showLongBalance: false
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import './PreFooter.scss';
const PreFooter = () => {
const PreFooter: React.SFC<{}> = () => {
return (
<section className="pre-footer">
<div className="container">

View File

@ -108,7 +108,7 @@ interface State {
isOpen: boolean;
}
export default class Footer extends React.Component<Props, State> {
export default class Footer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { isOpen: false };

View File

@ -36,7 +36,7 @@ interface State {
password: string;
}
export default class CustomNodeModal extends React.Component<Props, State> {
export default class CustomNodeModal extends React.PureComponent<Props, State> {
public state: State = {
name: '',
url: '',

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import NavigationLink from './NavigationLink';
import { knowledgeBaseURL } from 'config';
import './Navigation.scss';
@ -58,7 +58,7 @@ interface BorderStyle {
borderTopColor?: string;
}
export default class Navigation extends Component<Props, State> {
export default class Navigation extends PureComponent<Props, State> {
public state = {
showLeftArrow: false,
showRightArrow: false

View File

@ -10,7 +10,7 @@ interface Props extends RouteComponentProps<{}> {
isHomepage: boolean;
}
class NavigationLink extends React.Component<Props, {}> {
class NavigationLink extends React.PureComponent<Props, {}> {
public render() {
const { link, location, isHomepage } = this.props;
const isExternalLink = link.to.includes('http');

View File

@ -7,7 +7,7 @@ import {
} from 'actions/config';
import logo from 'assets/images/logo-myetherwallet.svg';
import { Dropdown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { TSetGasPriceField } from 'actions/transaction';
@ -49,7 +49,7 @@ interface State {
isAddingCustomNode: boolean;
}
export default class Header extends Component<Props, State> {
export default class Header extends PureComponent<Props, State> {
public state = {
isAddingCustomNode: false
};

View File

@ -14,7 +14,7 @@ interface Props {
match: RouteComponentProps<{}>['match'];
}
export default class SubTabs extends React.Component<Props> {
export default class SubTabs extends React.PureComponent<Props> {
public render() {
const { tabs, match } = this.props;
const currentPath = match.url;

View File

@ -1,9 +1,12 @@
import React from 'react';
import BN from 'bn.js';
import { translateRaw } from 'translations';
import { connect } from 'react-redux';
import {
inputGasPrice,
TInputGasPrice,
inputGasPriceIntent,
TInputGasPriceIntent,
getNonceRequested,
TGetNonceRequested,
reset,
@ -12,6 +15,7 @@ import {
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { Units } from 'libs/units';
import SimpleGas from './components/SimpleGas';
import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas';
import './TXMetaDataPanel.scss';
@ -27,6 +31,7 @@ interface StateProps {
interface DispatchProps {
inputGasPrice: TInputGasPrice;
inputGasPriceIntent: TInputGasPriceIntent;
fetchCCRates: TFetchCCRates;
getNonceRequested: TGetNonceRequested;
reset: TReset;
@ -47,6 +52,7 @@ interface OwnProps {
type Props = DispatchProps & OwnProps & StateProps;
interface State {
gasPrice: AppState['transaction']['fields']['gasPrice'];
sliderState: SliderStates;
}
@ -56,6 +62,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
};
public state: State = {
gasPrice: this.props.gasPrice,
sliderState: (this.props as DefaultProps).initialState
};
@ -71,10 +78,14 @@ class TXMetaDataPanel extends React.Component<Props, State> {
if (this.props.offline && !nextProps.offline) {
this.props.fetchCCRates([this.props.network.unit]);
}
if (this.props.gasPrice !== nextProps.gasPrice) {
this.setState({ gasPrice: nextProps.gasPrice });
}
}
public render() {
const { offline, disableToggle, gasPrice, advancedGasOptions, className = '' } = this.props;
const { offline, disableToggle, advancedGasOptions, className = '' } = this.props;
const { gasPrice } = this.state;
const showAdvanced = this.state.sliderState === 'advanced' || offline;
return (
<div className={`Gas col-md-12 ${className}`}>
@ -85,7 +96,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
options={advancedGasOptions}
/>
) : (
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.handleGasPriceInput} />
)}
{!offline &&
@ -107,6 +118,15 @@ class TXMetaDataPanel extends React.Component<Props, State> {
private toggleAdvanced = () => {
this.setState({ sliderState: this.state.sliderState === 'advanced' ? 'simple' : 'advanced' });
};
private handleGasPriceInput = (raw: string) => {
const gasBn = new BN(raw);
const value = gasBn.mul(new BN(Units.gwei));
this.setState({
gasPrice: { raw, value }
});
this.props.inputGasPriceIntent(raw);
};
}
function mapStateToProps(state: AppState): StateProps {
@ -119,6 +139,7 @@ function mapStateToProps(state: AppState): StateProps {
export default connect(mapStateToProps, {
inputGasPrice,
inputGasPriceIntent,
fetchCCRates,
getNonceRequested,
reset

View File

@ -108,6 +108,7 @@ class AdvancedGas extends React.Component<Props, State> {
{feeSummary && (
<div className="AdvancedGas-fee-summary">
<FeeSummary
gasPrice={gasPrice}
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}

View File

@ -14,17 +14,20 @@ interface RenderData {
usd: React.ReactElement<string> | null;
}
interface Props {
// Redux props
gasPrice: AppState['transaction']['fields']['gasPrice'];
interface ReduxStateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates'];
network: AppState['config']['network'];
isOffline: AppState['config']['offline'];
// Component props
}
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
render(data: RenderData): React.ReactElement<string> | string;
}
type Props = OwnProps & ReduxStateProps;
class FeeSummary extends React.Component<Props> {
public render() {
const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
@ -67,9 +70,8 @@ class FeeSummary extends React.Component<Props> {
}
}
function mapStateToProps(state: AppState) {
function mapStateToProps(state: AppState): ReduxStateProps {
return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit,
rates: state.rates.rates,
network: getNetworkConfig(state),

View File

@ -3,7 +3,6 @@ import Slider from 'rc-slider';
import translate, { translateRaw } from 'translations';
import { gasPriceDefaults } from 'config';
import FeeSummary from './FeeSummary';
import { TInputGasPrice } from 'actions/transaction';
import './SimpleGas.scss';
import { AppState } from 'reducers';
import { getGasLimitEstimationTimedOut } from 'selectors/transaction';
@ -13,7 +12,7 @@ import { getIsWeb3Node } from 'selectors/config';
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
inputGasPrice: TInputGasPrice;
inputGasPrice(rawGas: string);
}
interface StateProps {
@ -41,7 +40,7 @@ class SimpleGas extends React.Component<Props> {
<div className="prompt-toggle-gas-limit">
<p className="small">
{isWeb3Node
? "Couldn't calculate gas limit, if you know what your doing, try setting manually in Advanced settings"
? "Couldn't calculate gas limit, if you know what you're doing, try setting manually in Advanced settings"
: "Couldn't calculate gas limit, try switching nodes"}
</p>
</div>
@ -62,6 +61,7 @@ class SimpleGas extends React.Component<Props> {
</div>
</div>
<FeeSummary
gasPrice={gasPrice}
render={({ fee, usd }) => (
<span>
{fee} {usd && <span>/ ${usd}</span>}

View File

@ -60,7 +60,7 @@ const customDPath: DPath = {
value: 'custom'
};
class DeterministicWalletsModalClass extends React.Component<Props, State> {
class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
public state: State = {
selectedAddress: '',
selectedAddrIndex: 0,

View File

@ -1,6 +1,6 @@
import React from 'react';
export class DigitalBitboxDecrypt extends React.Component<{}, {}> {
export class DigitalBitboxDecrypt extends React.PureComponent<{}, {}> {
public render() {
return <strong>Not yet implemented</strong>;
}

View File

@ -1,5 +1,5 @@
import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';
import { TShowNotification } from 'actions/notifications';
@ -25,7 +25,7 @@ function isValidFile(rawFile: File): boolean {
return fileType === '' || fileType === 'application/json';
}
export class KeystoreDecrypt extends Component {
export class KeystoreDecrypt extends PureComponent {
public props: {
value: KeystoreValue;
isWalletPending: boolean;

View File

@ -1,5 +1,5 @@
import './LedgerNano.scss';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet';
@ -31,7 +31,7 @@ interface State {
type Props = OwnProps & StateProps;
class LedgerNanoSDecryptClass extends Component<Props, State> {
class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',

View File

@ -1,5 +1,5 @@
import { mnemonicToSeed, validateMnemonic } from 'bip39';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters';
@ -27,7 +27,7 @@ interface State {
dPath: string;
}
class MnemonicDecryptClass extends Component<Props & StateProps, State> {
class MnemonicDecryptClass extends PureComponent<Props & StateProps, State> {
public state: State = {
phrase: '',
formattedPhrase: '',

View File

@ -1,6 +1,6 @@
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import { TogglablePassword } from 'components';
@ -45,7 +45,7 @@ interface Props {
onUnlock(): void;
}
export class PrivateKeyDecrypt extends Component<Props> {
export class PrivateKeyDecrypt extends PureComponent<Props> {
public render() {
const { key, password } = this.props.value;
const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password);

View File

@ -1,5 +1,5 @@
import { TrezorWallet, TREZOR_MINIMUM_FIRMWARE } from 'libs/wallet';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
@ -32,7 +32,7 @@ interface State {
type Props = OwnProps & StateProps;
class TrezorDecryptClass extends Component<Props, State> {
class TrezorDecryptClass extends PureComponent<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config';
import { isValidETHAddress } from 'libs/validators';
@ -12,7 +12,7 @@ interface State {
address: string;
}
export class ViewOnlyDecrypt extends Component<Props, State> {
export class ViewOnlyDecrypt extends PureComponent<Props, State> {
public state = {
address: ''
};

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import translate from 'translations';
import { NewTabLink } from 'components/ui';
import './Web3.scss';
@ -7,27 +7,20 @@ interface Props {
onUnlock(): void;
}
export class Web3Decrypt extends Component<Props> {
public render() {
return (
<div className="Web3Decrypt">
<div>
<button
className="Web3Decrypt-decrypt btn btn-primary btn-lg btn-block"
onClick={this.props.onUnlock}
>
{translate('ADD_MetaMask')}
</button>
</div>
export const Web3Decrypt: React.SFC<Props> = ({ onUnlock }) => (
<div className="Web3Decrypt">
<div>
<button className="Web3Decrypt-decrypt btn btn-primary btn-lg btn-block" onClick={onUnlock}>
{translate('ADD_MetaMask')}
</button>
</div>
<div>
<NewTabLink
className="Web3Decrypt-install btn btn-sm btn-default btn-block"
content={translate('Download MetaMask')}
href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en"
/>
</div>
</div>
);
}
}
<div>
<NewTabLink
className="Web3Decrypt-install btn btn-sm btn-default btn-block"
content={translate('Download MetaMask')}
href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en"
/>
</div>
</div>
);

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import DropdownShell from './DropdownShell';
import removeIcon from 'assets/images/icon-remove.svg';
@ -25,7 +25,7 @@ interface Props<T> {
onChange(value: T): void;
}
export default class ColorDropdown<T> extends Component<Props<T>, {}> {
export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
private dropdownShell: DropdownShell | null;
public render() {

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import DropdownShell from './DropdownShell';
@ -19,7 +19,7 @@ interface State {
search: string;
}
export default class DropdownComponent<T> extends Component<Props<T>, State> {
export default class DropdownComponent<T> extends PureComponent<Props<T>, State> {
public state = {
search: ''
};

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import classnames from 'classnames';
interface Props {
@ -14,7 +14,7 @@ interface State {
expanded: boolean;
}
export default class DropdownComponent extends Component<Props, State> {
export default class DropdownComponent extends PureComponent<Props, State> {
public static defaultProps = {
color: 'default',
size: 'sm'

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
export type ExpandHandler = (ev: React.FormEvent<HTMLAnchorElement>) => void;
@ -13,7 +13,7 @@ interface State {
const initialState: State = { expanded: false };
export class Expandable extends Component<Props, State> {
export class Expandable extends PureComponent<Props, State> {
public state: State = initialState;
public render() {

View File

@ -1,5 +1,5 @@
import closeIcon from 'assets/images/icon-x.svg';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import './Modal.scss';
export interface IButton {
@ -17,7 +17,7 @@ interface Props {
handleClose?(): void;
}
export default class Modal extends Component<Props, {}> {
export default class Modal extends PureComponent<Props, {}> {
private modalContent: HTMLElement | null = null;
public componentDidMount() {

View File

@ -13,7 +13,7 @@ interface State {
qr?: string;
}
export default class QRCode extends React.Component<Props, State> {
export default class QRCode extends React.PureComponent<Props, State> {
public state: State = {};
public componentWillMount() {
// Start generating QR codes immediately

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import Dropdown from './Dropdown';
interface Props {
@ -8,7 +8,7 @@ interface Props {
onChange(value: string): void;
}
export default class SimpleDropdown extends Component<Props, void> {
export default class SimpleDropdown extends PureComponent<Props, void> {
public render() {
const { options, value, onChange, ariaLabel } = this.props;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
interface Props {
value?: string;
@ -6,7 +6,7 @@ interface Props {
onChange(event: React.FormEvent<HTMLSpanElement>): void;
}
export default class SimpleSelect extends Component<Props, {}> {
export default class SimpleSelect extends PureComponent<Props, {}> {
public render() {
return (
<select

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import './SwapDropdown.scss';
import classnames from 'classnames';
@ -16,7 +16,7 @@ interface Props<T> {
onChange(value: T): void;
}
class SwapDropdown<T> extends Component<Props<T>, {}> {
class SwapDropdown<T> extends PureComponent<Props<T>, {}> {
public state = {
open: false
};

View File

@ -17,7 +17,7 @@ interface State {
isKeystoreModalOpen: boolean;
}
export default class WalletInfo extends React.Component<Props, State> {
export default class WalletInfo extends React.PureComponent<Props, State> {
public state = {
address: '',
privateKey: '',

View File

@ -1,12 +1,12 @@
import QRCode from 'qrcode.react';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
interface Props {
paymentAddress: string | null;
destinationAmount: number;
}
export default class BitcoinQR extends Component<Props, {}> {
export default class BitcoinQR extends PureComponent<Props, {}> {
public render() {
const { paymentAddress, destinationAmount } = this.props;
return (

View File

@ -7,7 +7,7 @@ import {
} from 'reducers/swap/types';
import SimpleButton from 'components/ui/SimpleButton';
import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from 'config/bity';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import { SwapDropdown } from 'components/ui';
@ -41,7 +41,7 @@ interface State {
type Props = StateProps & ActionProps;
export default class CurrencySwap extends Component<Props, State> {
export default class CurrencySwap extends PureComponent<Props, State> {
public state = {
disabled: true,
origin: {

View File

@ -7,7 +7,7 @@ import bityLogoWhite from 'assets/images/logo-bity-white.svg';
import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg';
import Spinner from 'components/ui/Spinner';
import { bityReferralURL, shapeshiftReferralURL } from 'config';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import './CurrentRates.scss';
import { SHAPESHIFT_WHITELIST } from 'api/shapeshift';
@ -22,7 +22,7 @@ interface Props {
shapeshiftRates: NormalizedShapeshiftRates;
}
export default class CurrentRates extends Component<Props> {
export default class CurrentRates extends PureComponent<Props> {
private shapeShiftRateCache = null;
public getRandomSSPairData = (

View File

@ -9,7 +9,7 @@ import {
TStopPollShapeshiftOrderStatus
} from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import BitcoinQR from './BitcoinQR';
import PaymentInfo from './PaymentInfo';
import SwapProgress from './SwapProgress';
@ -39,7 +39,7 @@ interface ReduxActionProps {
showNotification: TShowNotification;
}
export default class PartThree extends Component<ReduxActionProps & ReduxStateProps, {}> {
export default class PartThree extends PureComponent<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() {
const { provider } = this.props;
if (provider === 'shapeshift') {

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import { SwapInput } from 'reducers/swap/types';
import './PaymentInfo.scss';
@ -8,7 +8,7 @@ export interface Props {
paymentAddress: string | null;
}
export default class PaymentInfo extends Component<Props, {}> {
export default class PaymentInfo extends PureComponent<Props, {}> {
public render() {
const { origin } = this.props;
return (

View File

@ -10,7 +10,7 @@ import classnames from 'classnames';
import SimpleButton from 'components/ui/SimpleButton';
import { donationAddressMap } from 'config';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';
import './ReceivingAddress.scss';
@ -32,7 +32,7 @@ export interface ActionProps {
shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
}
export default class ReceivingAddress extends Component<StateProps & ActionProps, {}> {
export default class ReceivingAddress extends PureComponent<StateProps & ActionProps, {}> {
public onChangeDestinationAddress = (event: React.FormEvent<HTMLInputElement>) => {
const value = event.currentTarget.value;
this.props.destinationAddressSwap(value);

View File

@ -2,7 +2,7 @@ import React from 'react';
import './ShapeshiftBanner.scss';
import shapeshiftSvg from 'assets/images/logo-shapeshift.svg';
export default () => (
const ShapeshiftBanner: React.SFC<{}> = () => (
<div className="ShapeshiftBanner">
<div className="ShapeshiftBanner-banner">
<p>
@ -13,3 +13,5 @@ export default () => (
</div>
</div>
);
export default ShapeshiftBanner;

View File

@ -14,7 +14,7 @@ interface Props {
bityRates: NormalizedBityRates;
}
class SupportFooter extends React.Component<Props, {}> {
class SupportFooter extends React.PureComponent<Props, {}> {
public state = {
open: false
};

View File

@ -1,6 +1,6 @@
import { RestartSwapAction } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import classnames from 'classnames';
import { toFixedIfLarger } from 'utils/formatters';
@ -16,7 +16,7 @@ export interface SwapInfoHeaderProps {
restartSwap(): RestartSwapAction;
}
export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> {
export default class SwapInfoHeader extends PureComponent<SwapInfoHeaderProps, {}> {
public computedOriginDestinationRatio = () => {
const { origin, destination } = this.props;
if (!origin.amount || !destination.amount) {

View File

@ -2,7 +2,7 @@ import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate from 'translations';
import './SwapInfoHeader.scss';
@ -11,7 +11,7 @@ export interface SwapInfoHeaderTitleProps {
restartSwap(): RestartSwapAction;
}
export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitleProps, {}> {
export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTitleProps, {}> {
public render() {
const { provider } = this.props;
const logoToRender = provider === 'shapeshift' ? shapeshiftLogo : bityLogo;

View File

@ -1,6 +1,6 @@
import { TShowNotification } from 'actions/notifications';
import { bityConfig } from 'config/bity';
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import './SwapProgress.scss';
@ -19,7 +19,7 @@ export interface Props {
interface State {
hasShownViewTx: boolean;
}
export default class SwapProgress extends Component<Props, State> {
export default class SwapProgress extends PureComponent<Props, State> {
public state = {
hasShownViewTx: false
};

View File

@ -11,7 +11,7 @@ type Data = Buffer;
export const ETH_DECIMAL = 18;
const Units = {
export const Units = {
wei: '1',
kwei: '1000',
ada: '1000',

View File

@ -1,11 +1,18 @@
import { State, RequestStatus } from './typings';
import { TypeKeys as TK, ResetAction, NetworkAction } from 'actions/transaction';
import {
TypeKeys as TK,
ResetAction,
NetworkAction,
InputGasPriceAction,
InputGasPriceIntentAction
} from 'actions/transaction';
import { Action } from 'redux';
const INITIAL_STATE: State = {
gasEstimationStatus: null,
getFromStatus: null,
getNonceStatus: null
getNonceStatus: null,
gasPriceStatus: null
};
const getPostFix = (str: string) => {
@ -18,9 +25,17 @@ const nextState = (field: keyof State) => (state: State, action: Action): State
[field]: RequestStatus[getPostFix(action.type)]
});
const setGasPriceStatus = (state: State, gasPriceStatus: RequestStatus) => ({
...state,
gasPriceStatus
});
const reset = () => INITIAL_STATE;
export const network = (state: State = INITIAL_STATE, action: NetworkAction | ResetAction) => {
export const network = (
state: State = INITIAL_STATE,
action: NetworkAction | ResetAction | InputGasPriceAction | InputGasPriceIntentAction
) => {
switch (action.type) {
case TK.ESTIMATE_GAS_REQUESTED:
return nextState('gasEstimationStatus')(state, action);
@ -42,6 +57,14 @@ export const network = (state: State = INITIAL_STATE, action: NetworkAction | Re
return nextState('getNonceStatus')(state, action);
case TK.GET_NONCE_FAILED:
return nextState('getNonceStatus')(state, action);
// Not exactly "network" requests, but we want to show pending while
// gas price is subject to change
case TK.GAS_PRICE_INPUT_INTENT:
return setGasPriceStatus(state, RequestStatus.REQUESTED);
case TK.GAS_PRICE_INPUT:
return setGasPriceStatus(state, RequestStatus.SUCCEEDED);
case TK.RESET:
return reset();
default:

View File

@ -8,4 +8,5 @@ export interface State {
gasEstimationStatus: RequestStatus | null;
getFromStatus: RequestStatus | null;
getNonceStatus: RequestStatus | null;
gasPriceStatus: RequestStatus | null;
}

View File

@ -1,7 +1,8 @@
import BN from 'bn.js';
import { call, put, takeEvery } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { SagaIterator, delay } from 'redux-saga';
import {
inputGasPrice,
setDataField,
setGasLimitField,
setGasPriceField,
@ -11,6 +12,7 @@ import {
InputDataAction,
InputGasLimitAction,
InputGasPriceAction,
InputGasPriceIntentAction,
InputNonceAction,
TypeKeys
} from 'actions/transaction';
@ -38,6 +40,13 @@ export function* handleGasPriceInput({ payload }: InputGasPriceAction): SagaIter
);
}
export function* handleGasPriceInputIntent({ payload }: InputGasPriceIntentAction): SagaIterator {
yield call(delay, 300);
// Important to put and not fork handleGasPriceInput, we want
// action to go to reducers.
yield put(inputGasPrice(payload));
}
export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator {
const validNonce: boolean = yield call(isValidNonce, payload);
yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null }));
@ -47,5 +56,6 @@ export const fields = [
takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput),
takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput),
takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput),
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput)
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput),
takeLatest(TypeKeys.GAS_PRICE_INPUT_INTENT, handleGasPriceInputIntent)
];

View File

@ -10,10 +10,12 @@ const getGasLimit = (state: AppState) => getFields(state).gasLimit;
const getGasPrice = (state: AppState) => getFields(state).gasPrice;
const getValue = (state: AppState) => getFields(state).value;
const getNonce = (state: AppState) => getFields(state).nonce;
const getDataExists = (state: AppState) => {
const { value } = getData(state);
return !!value && value.length > 0;
};
const getValidGasCost = (state: AppState) => {
const gasCost = getGasCost(state);
const etherBalance = getEtherBalance(state);