Avoid useless renders (#3141)

* feat(eslint): Set react/jsx-no-bind: error

* refactor(notifications/setting_toggle): Do not use bind

* refactor(components/dropdown_menu): Do not use bind

* refactor(components/autosuggest_textarea): Do not use bind

* refactor(compose/privacy_dropdown): Do not use bind

* refactor(compose/upload_form): Do not use bind

* refactor(components/status): Do not use bind

* refactor(components/onboarding_modal): Do not use bind

* refactor: PR feedback

* chore(notifications/setting_toggle): Lint

* refactor: PR feedback
This commit is contained in:
Sorin Davidoi 2017-05-19 20:58:12 +02:00 committed by Eugen Rochko
parent 3da521a586
commit 1548695c83
8 changed files with 68 additions and 41 deletions

View File

@ -49,6 +49,7 @@ rules:
no-trailing-spaces: warn no-trailing-spaces: warn
react/jsx-wrap-multilines: error react/jsx-wrap-multilines: error
react/jsx-no-bind: error
react/self-closing-comp: error react/self-closing-comp: error
react/prop-types: error react/prop-types: error
react/no-multi-comp: off react/no-multi-comp: off

View File

@ -145,7 +145,8 @@ class AutosuggestTextarea extends ImmutablePureComponent {
}, 100); }, 100);
} }
onSuggestionClick (suggestion, e) { onSuggestionClick (e) {
const suggestion = Number(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus(); this.textarea.focus();
@ -204,8 +205,9 @@ class AutosuggestTextarea extends ImmutablePureComponent {
role='button' role='button'
tabIndex='0' tabIndex='0'
key={suggestion} key={suggestion}
data-index={suggestion}
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
onClick={this.onSuggestionClick.bind(this, suggestion)}> onClick={this.onSuggestionClick}>
<AutosuggestAccountContainer id={suggestion} /> <AutosuggestAccountContainer id={suggestion} />
</div> </div>
))} ))}

View File

@ -24,7 +24,8 @@ class DropdownMenu extends React.PureComponent {
this.dropdown = c; this.dropdown = c;
} }
handleClick = (i, e) => { handleClick = (e) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action } = this.props.items[i]; const { action } = this.props.items[i];
if (typeof action === 'function') { if (typeof action === 'function') {
@ -43,7 +44,7 @@ class DropdownMenu extends React.PureComponent {
return ( return (
<li className='dropdown__content-list-item' key={ text + i }> <li className='dropdown__content-list-item' key={ text + i }>
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'> <a href={href} target='_blank' rel='noopener' onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
{text} {text}
</a> </a>
</li> </li>

View File

@ -43,8 +43,9 @@ class Status extends ImmutablePureComponent {
this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} }
handleAccountClick = (id, e) => { handleAccountClick = (e) => {
if (e.button === 0) { if (e.button === 0) {
const id = Number(e.currentTarget.getAttribute('data-id'));
e.preventDefault(); e.preventDefault();
this.context.router.push(`/accounts/${id}`); this.context.router.push(`/accounts/${id}`);
} }
@ -72,7 +73,7 @@ class Status extends ImmutablePureComponent {
<div className='status__wrapper'> <div className='status__wrapper'>
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
</div> </div>
<Status {...other} wrapped={true} status={status.get('reblog')} account={status.get('account')} /> <Status {...other} wrapped={true} status={status.get('reblog')} account={status.get('account')} />
@ -103,7 +104,7 @@ class Status extends ImmutablePureComponent {
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div> </div>
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>

View File

@ -36,7 +36,8 @@ class PrivacyDropdown extends React.PureComponent {
this.setState({ open: !this.state.open }); this.setState({ open: !this.state.open });
} }
handleClick = (value, e) => { handleClick = (e) => {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault(); e.preventDefault();
this.setState({ open: false }); this.setState({ open: false });
this.props.onChange(value); this.props.onChange(value);
@ -80,7 +81,7 @@ class PrivacyDropdown extends React.PureComponent {
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div> <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div>
<div className='privacy-dropdown__dropdown'> <div className='privacy-dropdown__dropdown'>
{options.map(item => {options.map(item =>
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> <div role='button' tabIndex='0' key={item.value} data-index={item.value} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div> <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'> <div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong> <strong>{item.shortText}</strong>

View File

@ -18,6 +18,11 @@ class UploadForm extends React.PureComponent {
intl: PropTypes.object.isRequired intl: PropTypes.object.isRequired
}; };
onRemoveFile = (e) => {
const id = Number(e.currentTarget.parentElement.getAttribute('data-id'));
this.props.onRemoveFile(id);
}
render () { render () {
const { intl, media } = this.props; const { intl, media } = this.props;
@ -25,8 +30,8 @@ class UploadForm extends React.PureComponent {
<div className='compose-form__upload' key={attachment.get('id')}> <div className='compose-form__upload' key={attachment.get('id')}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => {({ scale }) =>
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${attachment.get('preview_url')})` }}> <div className='compose-form__upload-thumbnail' data-id={attachment.get('id')} style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${attachment.get('preview_url')})` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} /> <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.onRemoveFile} />
</div> </div>
} }
</Motion> </Motion>

View File

@ -3,19 +3,31 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => ( class SettingToggle extends React.PureComponent {
<label htmlFor={htmlFor} className='setting-toggle__label'>
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
<span className='setting-toggle'>{label}</span>
</label>
);
SettingToggle.propTypes = { static propTypes = {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
settingKey: PropTypes.array.isRequired, settingKey: PropTypes.array.isRequired,
label: PropTypes.node.isRequired, label: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
htmlFor: PropTypes.string htmlFor: PropTypes.string
}; }
onChange = (e) => {
this.props.onChange(this.props.settingKey, e.target.checked);
}
render () {
const { settings, settingKey, label, onChange, htmlFor = '' } = this.props;
return (
<label htmlFor={htmlFor} className='setting-toggle__label'>
<Toggle checked={settings.getIn(settingKey)} onChange={this.onChange} />
<span className='setting-toggle'>{label}</span>
</label>
);
}
}
export default SettingToggle; export default SettingToggle;

View File

@ -11,6 +11,8 @@ import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header'; import ColumnHeader from './column_header';
import Immutable from 'immutable'; import Immutable from 'immutable';
const noop = () => { };
const messages = defineMessages({ const messages = defineMessages({
home_title: { id: 'column.home', defaultMessage: 'Home' }, home_title: { id: 'column.home', defaultMessage: 'Home' },
notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@ -48,14 +50,14 @@ const PageTwo = ({ me }) => (
suggestions={Immutable.List()} suggestions={Immutable.List()}
mentionedDomains={[]} mentionedDomains={[]}
spoiler={false} spoiler={false}
onChange={() => {}} onChange={noop}
onSubmit={() => {}} onSubmit={noop}
onPaste={() => {}} onPaste={noop}
onPickEmoji={() => {}} onPickEmoji={noop}
onChangeSpoilerText={() => {}} onChangeSpoilerText={noop}
onClearSuggestions={() => {}} onClearSuggestions={noop}
onFetchSuggestions={() => {}} onFetchSuggestions={noop}
onSuggestionSelected={() => {}} onSuggestionSelected={noop}
/> />
</div> </div>
@ -72,10 +74,10 @@ const PageThree = ({ me, domain }) => (
<div className='figure non-interactive'> <div className='figure non-interactive'>
<Search <Search
value='' value=''
onChange={() => {}} onChange={noop}
onSubmit={() => {}} onSubmit={noop}
onClear={() => {}} onClear={noop}
onShow={() => {}} onShow={noop}
/> />
<div className='pseudo-drawer'> <div className='pseudo-drawer'>
@ -182,12 +184,14 @@ class OnboardingModal extends React.PureComponent {
this.props.onClose(); this.props.onClose();
} }
handleDot = (i, e) => { handleDot = (e) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.setState({ currentIndex: i }); this.setState({ currentIndex: i });
} }
handleNext = (maxNum, e) => { handleNext = (e) => {
const maxNum = Number(e.currentTarget.getAttribute('data-length'));
e.preventDefault(); e.preventDefault();
if (this.state.currentIndex < maxNum - 1) { if (this.state.currentIndex < maxNum - 1) {
@ -214,9 +218,9 @@ class OnboardingModal extends React.PureComponent {
let nextOrDoneBtn; let nextOrDoneBtn;
if(hasMore) { if(hasMore) {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>; nextOrDoneBtn = <a href='#' data-length={pages.length} onClick={this.handleNext} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
} else { } else {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.done' defaultMessage='Done' /></a>; nextOrDoneBtn = <a href='#' data-length={pages.length} onClick={this.handleNext} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.done' defaultMessage='Done' /></a>;
} }
const styles = pages.map((page, i) => ({ const styles = pages.map((page, i) => ({
@ -242,7 +246,7 @@ class OnboardingModal extends React.PureComponent {
</div> </div>
<div className='onboarding-modal__dots'> <div className='onboarding-modal__dots'>
{pages.map((_, i) => <div key={i} role='button' tabIndex='0' onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)} {pages.map((_, i) => <div key={i} role='button' tabIndex='0' data-index={i} onClick={this.handleDot} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
</div> </div>
<div> <div>