Merge pull request #1480 from 0xProject/feature/instant/performance-boost

[instant] No more laggy input
This commit is contained in:
Francesco Agosti
2019-01-03 13:36:18 +01:00
committed by GitHub
30 changed files with 153 additions and 79 deletions

View File

@@ -30,3 +30,5 @@ export const AmountPlaceholder: React.StatelessComponent<AmountPlaceholderProps>
return <PlainPlaceholder color={props.color} />; return <PlainPlaceholder color={props.color} />;
} }
}; };
AmountPlaceholder.displayName = 'AmountPlaceholder';

View File

@@ -11,6 +11,7 @@ export interface SlideAnimationProps {
slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>; slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
zIndex?: OptionallyScreenSpecific<number>; zIndex?: OptionallyScreenSpecific<number>;
height?: string; height?: string;
onAnimationEnd?: () => void;
} }
export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => { export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => {
@@ -19,8 +20,15 @@ export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = pro
} }
const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
return ( return (
<PositionAnimation height={props.height} positionSettings={positionSettings} zIndex={props.zIndex}> <PositionAnimation
onAnimationEnd={props.onAnimationEnd}
height={props.height}
positionSettings={positionSettings}
zIndex={props.zIndex}
>
{props.children} {props.children}
</PositionAnimation> </PositionAnimation>
); );
}; };
SlideAnimation.displayName = 'SlideAnimation';

View File

@@ -32,7 +32,7 @@ export interface BuyButtonProps {
onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void;
} }
export class BuyButton extends React.Component<BuyButtonProps> { export class BuyButton extends React.PureComponent<BuyButtonProps> {
public static defaultProps = { public static defaultProps = {
onClick: util.boundNoop, onClick: util.boundNoop,
onBuySuccess: util.boundNoop, onBuySuccess: util.boundNoop,

View File

@@ -31,3 +31,5 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> =
} }
return null; return null;
}; };
BuyOrderProgress.displayName = 'BuyOrderProgress';

View File

@@ -71,3 +71,5 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
/> />
); );
}; };
BuyOrderStateButtons.displayName = 'BuyOrderStateButtons';

View File

@@ -31,7 +31,7 @@ export interface ERC20AssetAmountInputState {
currentFontSizePx: number; currentFontSizePx: number;
} }
export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInputProps, ERC20AssetAmountInputState> { export class ERC20AssetAmountInput extends React.PureComponent<ERC20AssetAmountInputProps, ERC20AssetAmountInputState> {
public static defaultProps = { public static defaultProps = {
onChange: util.boundNoop, onChange: util.boundNoop,
isDisabled: false, isDisabled: false,

View File

@@ -21,12 +21,12 @@ export interface ERC20TokenSelectorState {
searchQuery: string; searchQuery: string;
} }
export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> { export class ERC20TokenSelector extends React.PureComponent<ERC20TokenSelectorProps> {
public state: ERC20TokenSelectorState = { public state: ERC20TokenSelectorState = {
searchQuery: '', searchQuery: '',
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const { tokens, onTokenSelect } = this.props; const { tokens } = this.props;
return ( return (
<Container height="100%"> <Container height="100%">
<Container marginBottom="10px"> <Container marginBottom="10px">
@@ -42,12 +42,11 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
tabIndex={-1} tabIndex={-1}
/> />
<Container overflow="scroll" height="calc(100% - 90px)" marginTop="10px"> <Container overflow="scroll" height="calc(100% - 90px)" marginTop="10px">
{_.map(tokens, token => { <TokenRowFilter
if (!this._isTokenQueryMatch(token)) { tokens={tokens}
return null; onClick={this._handleTokenClick}
} searchQuery={this.state.searchQuery}
return <TokenSelectorRow key={token.assetData} token={token} onClick={onTokenSelect} />; />
})}
</Container> </Container>
</Container> </Container>
); );
@@ -59,8 +58,32 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
}); });
analytics.trackTokenSelectorSearched(searchQuery); analytics.trackTokenSelectorSearched(searchQuery);
}; };
private readonly _handleTokenClick = (token: ERC20Asset): void => {
this.props.onTokenSelect(token);
};
}
interface TokenRowFilterProps {
tokens: ERC20Asset[];
onClick: (token: ERC20Asset) => void;
searchQuery: string;
}
class TokenRowFilter extends React.Component<TokenRowFilterProps> {
public render(): React.ReactNode {
return _.map(this.props.tokens, token => {
if (!this._isTokenQueryMatch(token)) {
return null;
}
return <TokenSelectorRow key={token.assetData} token={token} onClick={this.props.onClick} />;
});
}
public shouldComponentUpdate(nextProps: TokenRowFilterProps): boolean {
const arePropsDeeplyEqual = _.isEqual(nextProps, this.props);
return !arePropsDeeplyEqual;
}
private readonly _isTokenQueryMatch = (token: ERC20Asset): boolean => { private readonly _isTokenQueryMatch = (token: ERC20Asset): boolean => {
const { searchQuery } = this.state; const { searchQuery } = this.props;
const searchQueryLowerCase = searchQuery.toLowerCase().trim(); const searchQueryLowerCase = searchQuery.toLowerCase().trim();
if (searchQueryLowerCase === '') { if (searchQueryLowerCase === '') {
return true; return true;
@@ -76,7 +99,7 @@ interface TokenSelectorRowProps {
onClick: (token: ERC20Asset) => void; onClick: (token: ERC20Asset) => void;
} }
class TokenSelectorRow extends React.Component<TokenSelectorRowProps> { class TokenSelectorRow extends React.PureComponent<TokenSelectorRowProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const { token } = this.props; const { token } = this.props;
const circleColor = token.metaData.primaryColor || 'black'; const circleColor = token.metaData.primaryColor || 'black';
@@ -131,8 +154,9 @@ const getTokenIcon = (symbol: string): React.StatelessComponent | undefined => {
} }
}; };
const TokenSelectorRowIcon: React.StatelessComponent<TokenSelectorRowIconProps> = props => { class TokenSelectorRowIcon extends React.PureComponent<TokenSelectorRowIconProps> {
const { token } = props; public render(): React.ReactNode {
const { token } = this.props;
const iconUrlIfExists = token.metaData.iconUrl; const iconUrlIfExists = token.metaData.iconUrl;
const TokenIcon = getTokenIcon(token.metaData.symbol); const TokenIcon = getTokenIcon(token.metaData.symbol);
@@ -148,4 +172,5 @@ const TokenSelectorRowIcon: React.StatelessComponent<TokenSelectorRowIconProps>
</Text> </Text>
); );
} }
}; }
}

View File

@@ -18,7 +18,7 @@ import { Button } from './ui/button';
export interface InstallWalletPanelContentProps {} export interface InstallWalletPanelContentProps {}
export class InstallWalletPanelContent extends React.Component<InstallWalletPanelContentProps> { export class InstallWalletPanelContent extends React.PureComponent<InstallWalletPanelContentProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const panelProps = this._getStandardPanelContentProps(); const panelProps = this._getStandardPanelContentProps();
return <StandardPanelContent {...panelProps} />; return <StandardPanelContent {...panelProps} />;

View File

@@ -28,7 +28,7 @@ const ICON_WIDTH = 34;
const ICON_HEIGHT = 34; const ICON_HEIGHT = 34;
const ICON_COLOR = ColorOption.white; const ICON_COLOR = ColorOption.white;
export class InstantHeading extends React.Component<InstantHeadingProps, {}> { export class InstantHeading extends React.PureComponent<InstantHeadingProps, {}> {
public render(): React.ReactNode { public render(): React.ReactNode {
const iconOrAmounts = this._renderIcon() || this._renderAmountsSection(); const iconOrAmounts = this._renderIcon() || this._renderAmountsSection();
return ( return (

View File

@@ -26,7 +26,7 @@ export interface OrderDetailsProps {
onBaseCurrencySwitchEth: () => void; onBaseCurrencySwitchEth: () => void;
onBaseCurrencySwitchUsd: () => void; onBaseCurrencySwitchUsd: () => void;
} }
export class OrderDetails extends React.Component<OrderDetailsProps> { export class OrderDetails extends React.PureComponent<OrderDetailsProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice(); const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice();
return ( return (
@@ -200,7 +200,7 @@ export interface OrderDetailsRowProps {
primaryValue: React.ReactNode; primaryValue: React.ReactNode;
secondaryValue?: React.ReactNode; secondaryValue?: React.ReactNode;
} }
export class OrderDetailsRow extends React.Component<OrderDetailsRowProps, {}> { export class OrderDetailsRow extends React.PureComponent<OrderDetailsRowProps, {}> {
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>

View File

@@ -24,7 +24,7 @@ export interface PaymentMethodProps {
onUnlockWalletClick: () => void; onUnlockWalletClick: () => void;
} }
export class PaymentMethod extends React.Component<PaymentMethodProps> { export class PaymentMethod extends React.PureComponent<PaymentMethodProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<Container width="100%" height="120px" padding="20px 20px 0px 20px"> <Container width="100%" height="120px" padding="20px 20px 0px 20px">

View File

@@ -16,7 +16,7 @@ export interface PaymentMethodDropdownProps {
network: Network; network: Network;
} }
export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdownProps> { export class PaymentMethodDropdown extends React.PureComponent<PaymentMethodDropdownProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const { accountAddress, accountEthBalanceInWei } = this.props; const { accountAddress, accountEthBalanceInWei } = this.props;
const value = format.ethAddress(accountAddress); const value = format.ethAddress(accountAddress);

View File

@@ -14,3 +14,5 @@ export const PlacingOrderButton: React.StatelessComponent<{}> = props => (
Placing Order&hellip; Placing Order&hellip;
</Button> </Button>
); );
PlacingOrderButton.displayName = 'PlacingOrderButton';

View File

@@ -26,7 +26,7 @@ interface ScalingAmountInputState {
} }
const { stringToMaybeBigNumber, areMaybeBigNumbersEqual } = maybeBigNumberUtil; const { stringToMaybeBigNumber, areMaybeBigNumbersEqual } = maybeBigNumberUtil;
export class ScalingAmountInput extends React.Component<ScalingAmountInputProps, ScalingAmountInputState> { export class ScalingAmountInput extends React.PureComponent<ScalingAmountInputProps, ScalingAmountInputState> {
public static defaultProps = { public static defaultProps = {
onAmountChange: util.boundNoop, onAmountChange: util.boundNoop,
onFontSizeChange: util.boundNoop, onFontSizeChange: util.boundNoop,

View File

@@ -51,7 +51,7 @@ const defaultScalingSettings: ScalingSettings = {
additionalInputSpaceInCh: 0.4, additionalInputSpaceInCh: 0.4,
}; };
export class ScalingInput extends React.Component<ScalingInputProps> { export class ScalingInput extends React.PureComponent<ScalingInputProps> {
public static defaultProps = { public static defaultProps = {
onChange: util.boundNoop, onChange: util.boundNoop,
onFontSizeChange: util.boundNoop, onFontSizeChange: util.boundNoop,

View File

@@ -24,3 +24,4 @@ export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = p
SecondaryButton.defaultProps = { SecondaryButton.defaultProps = {
width: '100%', width: '100%',
}; };
SecondaryButton.displayName = 'SecondaryButton';

View File

@@ -18,3 +18,4 @@ export const SectionHeader: React.StatelessComponent<SectionHeaderProps> = props
</Text> </Text>
); );
}; };
SectionHeader.displayName = 'SectionHeader';

View File

@@ -38,6 +38,8 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
</Container> </Container>
); );
Error.displayName = 'Error';
export interface SlidingErrorProps extends ErrorProps { export interface SlidingErrorProps extends ErrorProps {
animationState: SlideAnimationState; animationState: SlideAnimationState;
} }
@@ -94,3 +96,5 @@ export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props =
</SlideAnimation> </SlideAnimation>
); );
}; };
SlidingError.displayName = 'SlidingError';

View File

@@ -26,15 +26,19 @@ export const Panel: React.StatelessComponent<PanelProps> = ({ children, onClose
</Container> </Container>
); );
Panel.displayName = 'Panel';
export interface SlidingPanelProps extends PanelProps { export interface SlidingPanelProps extends PanelProps {
animationState: SlideAnimationState; animationState: SlideAnimationState;
onAnimationEnd?: () => void;
} }
export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props => { export class SlidingPanel extends React.PureComponent<SlidingPanelProps> {
if (props.animationState === 'none') { public render(): React.ReactNode {
if (this.props.animationState === 'none') {
return null; return null;
} }
const { animationState, ...rest } = props; const { animationState, onAnimationEnd, ...rest } = this.props;
const slideAmount = '100%'; const slideAmount = '100%';
const slideUpSettings: PositionAnimationSettings = { const slideUpSettings: PositionAnimationSettings = {
duration: '0.3s', duration: '0.3s',
@@ -60,8 +64,10 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props =
slideOutSettings={slideDownSettings} slideOutSettings={slideDownSettings}
animationState={animationState} animationState={animationState}
height="100%" height="100%"
onAnimationEnd={onAnimationEnd}
> >
<Panel {...rest} /> <Panel {...rest} />
</SlideAnimation> </SlideAnimation>
); );
}; }
}

View File

@@ -71,3 +71,5 @@ export const StandardPanelContent: React.StatelessComponent<StandardPanelContent
<Container>{action}</Container> <Container>{action}</Container>
</Container> </Container>
); );
StandardPanelContent.displayName = 'StandardPanelContent';

View File

@@ -9,7 +9,7 @@ export interface StandardSlidingPanelProps extends StandardSlidingPanelSettings
onClose: () => void; onClose: () => void;
} }
export class StandardSlidingPanel extends React.Component<StandardSlidingPanelProps> { export class StandardSlidingPanel extends React.PureComponent<StandardSlidingPanelProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const { animationState, content, onClose } = this.props; const { animationState, content, onClose } = this.props;
return ( return (

View File

@@ -16,7 +16,7 @@ interface TimeCounterState {
elapsedSeconds: number; elapsedSeconds: number;
} }
export class TimeCounter extends React.Component<TimeCounterProps, TimeCounterState> { export class TimeCounter extends React.PureComponent<TimeCounterProps, TimeCounterState> {
public state = { public state = {
elapsedSeconds: 0, elapsedSeconds: 0,
}; };

View File

@@ -17,7 +17,7 @@ export interface TimedProgressBarProps {
* Goes from 0% -> PROGRESS_STALL_AT_WIDTH over time of expectedTimeMs * Goes from 0% -> PROGRESS_STALL_AT_WIDTH over time of expectedTimeMs
* When hasEnded set to true, goes to 100% through animation of PROGRESS_FINISH_ANIMATION_TIME_MS length of time * When hasEnded set to true, goes to 100% through animation of PROGRESS_FINISH_ANIMATION_TIME_MS length of time
*/ */
export class TimedProgressBar extends React.Component<TimedProgressBarProps, {}> { export class TimedProgressBar extends React.PureComponent<TimedProgressBarProps, {}> {
private readonly _barRef = React.createRef<HTMLDivElement>(); private readonly _barRef = React.createRef<HTMLDivElement>();
public render(): React.ReactNode { public render(): React.ReactNode {

View File

@@ -26,7 +26,7 @@ export interface DropdownState {
isOpen: boolean; isOpen: boolean;
} }
export class Dropdown extends React.Component<DropdownProps, DropdownState> { export class Dropdown extends React.PureComponent<DropdownProps, DropdownState> {
public static defaultProps = { public static defaultProps = {
items: [], items: [],
}; };
@@ -138,3 +138,5 @@ export const DropdownItem: React.StatelessComponent<DropdownItemProps> = ({ text
</Text> </Text>
</Container> </Container>
); );
DropdownItem.displayName = 'DropdownItem';

View File

@@ -45,3 +45,5 @@ WalletPrompt.defaultProps = {
primaryColor: ColorOption.darkOrange, primaryColor: ColorOption.darkOrange,
secondaryColor: ColorOption.lightOrange, secondaryColor: ColorOption.lightOrange,
}; };
WalletPrompt.displayName = 'WalletPrompt';

View File

@@ -17,3 +17,5 @@ export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props
</div> </div>
); );
}; };
ZeroExInstant.displayName = 'ZeroExInstant';

View File

@@ -24,7 +24,10 @@ export interface ZeroExInstantContainerState {
tokenSelectionPanelAnimationState: SlideAnimationState; tokenSelectionPanelAnimationState: SlideAnimationState;
} }
export class ZeroExInstantContainer extends React.Component<ZeroExInstantContainerProps, ZeroExInstantContainerState> { export class ZeroExInstantContainer extends React.PureComponent<
ZeroExInstantContainerProps,
ZeroExInstantContainerState
> {
public state = { public state = {
tokenSelectionPanelAnimationState: 'none' as SlideAnimationState, tokenSelectionPanelAnimationState: 'none' as SlideAnimationState,
}; };
@@ -60,6 +63,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
<SlidingPanel <SlidingPanel
animationState={this.state.tokenSelectionPanelAnimationState} animationState={this.state.tokenSelectionPanelAnimationState}
onClose={this._handlePanelCloseClickedX} onClose={this._handlePanelCloseClickedX}
onAnimationEnd={this._handleSlidingPanelAnimationEnd}
> >
<AvailableERC20TokenSelector onTokenSelect={this._handlePanelCloseAfterChose} /> <AvailableERC20TokenSelector onTokenSelect={this._handlePanelCloseAfterChose} />
</SlidingPanel> </SlidingPanel>
@@ -98,4 +102,11 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
tokenSelectionPanelAnimationState: 'slidOut', tokenSelectionPanelAnimationState: 'slidOut',
}); });
}; };
private readonly _handleSlidingPanelAnimationEnd = (): void => {
if (this.state.tokenSelectionPanelAnimationState === 'slidOut') {
// When the slidOut animation completes, don't keep the panel mounted.
// Performance optimization
this.setState({ tokenSelectionPanelAnimationState: 'none' });
}
};
} }

View File

@@ -49,3 +49,5 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay
</ZeroExInstantProvider> </ZeroExInstantProvider>
); );
}; };
ZeroExInstantOverlay.displayName = 'ZeroExInstantOverlay';

View File

@@ -21,7 +21,7 @@ import { providerStateFactory } from '../util/provider_state_factory';
export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig; export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig;
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> { export class ZeroExInstantProvider extends React.PureComponent<ZeroExInstantProviderProps> {
private readonly _store: Store; private readonly _store: Store;
private _accountUpdateHeartbeat?: Heartbeater; private _accountUpdateHeartbeat?: Heartbeater;
private _buyQuoteHeartbeat?: Heartbeater; private _buyQuoteHeartbeat?: Heartbeater;

View File

@@ -14,7 +14,7 @@ import { zIndex } from '../style/z_index';
import { Asset, DisplayStatus, Omit, SlideAnimationState } from '../types'; import { Asset, DisplayStatus, Omit, SlideAnimationState } from '../types';
import { errorFlasher } from '../util/error_flasher'; import { errorFlasher } from '../util/error_flasher';
export interface LatestErrorComponentProps { interface LatestErrorComponentProps {
asset?: Asset; asset?: Asset;
latestErrorMessage?: string; latestErrorMessage?: string;
animationState: SlideAnimationState; animationState: SlideAnimationState;
@@ -22,7 +22,7 @@ export interface LatestErrorComponentProps {
onOverlayClick: () => void; onOverlayClick: () => void;
} }
export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => { const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => {
if (!props.latestErrorMessage) { if (!props.latestErrorMessage) {
// Render a hidden SlidingError such that instant does not move when a real error is rendered. // Render a hidden SlidingError such that instant does not move when a real error is rendered.
return ( return (