Use reselect to memoize denormalization in UI state

Also upgrade react-redux to latest version. This is a performance update
This commit is contained in:
Eugen Rochko 2016-10-08 00:01:22 +02:00
parent 1f650d327d
commit ef9d4f4e06
12 changed files with 136 additions and 80 deletions

View file

@ -3,6 +3,7 @@
window.React = require('react'); window.React = require('react');
window.ReactDOM = require('react-dom'); window.ReactDOM = require('react-dom');
window.Perf = require('react-addons-perf');
//= require_tree ./components //= require_tree ./components

View file

@ -28,7 +28,7 @@ const StatusList = React.createClass({
const { statuses, onScrollToBottom, ...other } = this.props; const { statuses, onScrollToBottom, ...other } = this.props;
return ( return (
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable' onScroll={this.handleScroll}> <div style={{ overflowY: 'scroll', flex: '1 1 auto', overflowX: 'hidden' }} className='scrollable' onScroll={this.handleScroll}>
<div> <div>
{statuses.map((status) => { {statuses.map((status) => {
return <Status key={status.get('id')} {...other} status={status} />; return <Status key={status.get('id')} {...other} status={status} />;

View file

@ -20,22 +20,18 @@ import {
} from '../../actions/interactions'; } from '../../actions/interactions';
import Header from './components/header'; import Header from './components/header';
import { import {
selectStatus, getAccountTimeline,
selectAccount getAccount
} from '../../reducers/timelines'; } from '../../selectors';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
function selectStatuses(state, accountId) {
return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List([])).map(id => selectStatus(state, id)).filterNot(status => status === null);
};
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
account: selectAccount(state, Number(props.params.accountId)), account: getAccount(state, Number(props.params.accountId)),
statuses: selectStatuses(state, Number(props.params.accountId)), statuses: getAccountTimeline(state, Number(props.params.accountId)),
me: state.getIn(['timelines', 'me']) me: state.getIn(['timelines', 'me'])
}); });

View file

@ -1,33 +1,35 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import Immutable from 'immutable'; import Immutable from 'immutable';
import { selectStatus } from '../../reducers/timelines'; import { makeGetTimeline } from '../../selectors';
import { import {
updateTimeline, updateTimeline,
refreshTimeline, refreshTimeline,
expandTimeline expandTimeline
} from '../../actions/timelines'; } from '../../actions/timelines';
import { deleteStatus } from '../../actions/statuses'; import { deleteStatus } from '../../actions/statuses';
import { replyCompose } from '../../actions/compose'; import { replyCompose } from '../../actions/compose';
import { import {
favourite, favourite,
reblog, reblog,
unreblog, unreblog,
unfavourite unfavourite
} from '../../actions/interactions'; } from '../../actions/interactions';
function selectStatuses(state) { const makeMapStateToProps = () => {
return state.getIn(['timelines', 'public'], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null); const getTimeline = makeGetTimeline();
const mapStateToProps = (state) => ({
statuses: getTimeline(state, 'public'),
me: state.getIn(['timelines', 'me'])
});
return mapStateToProps;
}; };
const mapStateToProps = (state) => ({
statuses: selectStatuses(state),
me: state.getIn(['timelines', 'me'])
});
const PublicTimeline = React.createClass({ const PublicTimeline = React.createClass({
propTypes: { propTypes: {
@ -100,4 +102,4 @@ const PublicTimeline = React.createClass({
}); });
export default connect(mapStateToProps)(PublicTimeline); export default connect(makeMapStateToProps)(PublicTimeline);

View file

@ -10,16 +10,16 @@ import ActionBar from './components/action_bar';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { favourite, reblog } from '../../actions/interactions'; import { favourite, reblog } from '../../actions/interactions';
import { replyCompose } from '../../actions/compose'; import { replyCompose } from '../../actions/compose';
import { selectStatus } from '../../reducers/timelines'; import {
getStatus,
function selectStatuses(state, ids) { getStatusAncestors,
return ids.map(id => selectStatus(state, id)).filterNot(status => status === null); getStatusDescendants
}; } from '../../selectors';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: selectStatus(state, Number(props.params.statusId)), status: getStatus(state, Number(props.params.statusId)),
ancestors: selectStatuses(state, state.getIn(['timelines', 'ancestors', Number(props.params.statusId)], Immutable.OrderedSet())), ancestors: getStatusAncestors(state, Number(props.params.statusId)),
descendants: selectStatuses(state, state.getIn(['timelines', 'descendants', Number(props.params.statusId)], Immutable.OrderedSet())), descendants: getStatusDescendants(state, Number(props.params.statusId)),
me: state.getIn(['timelines', 'me']) me: state.getIn(['timelines', 'me'])
}); });

View file

@ -1,14 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form'; import ComposeForm from '../components/compose_form';
import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose'; import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose';
import { selectStatus } from '../../../reducers/timelines'; import { getStatus } from '../../../selectors';
const mapStateToProps = function (state, props) { const mapStateToProps = function (state, props) {
return { return {
text: state.getIn(['compose', 'text']), text: state.getIn(['compose', 'text']),
is_submitting: state.getIn(['compose', 'is_submitting']), is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']), is_uploading: state.getIn(['compose', 'is_uploading']),
in_reply_to: selectStatus(state, state.getIn(['compose', 'in_reply_to'])) in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to']))
}; };
}; };

View file

@ -4,14 +4,10 @@ import {
dismissNotification, dismissNotification,
clearNotifications clearNotifications
} from '../../../actions/notifications'; } from '../../../actions/notifications';
import { getNotifications } from '../../../selectors';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
notifications: state.get('notifications').map((item, i) => ({ notifications: getNotifications(state)
message: item.get('message'),
title: item.get('title'),
key: item.get('key'),
dismissAfter: 5000
})).toJS()
}); });
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {

View file

@ -8,14 +8,18 @@ import {
unfavourite unfavourite
} from '../../../actions/interactions'; } from '../../../actions/interactions';
import { expandTimeline } from '../../../actions/timelines'; import { expandTimeline } from '../../../actions/timelines';
import { selectStatus } from '../../../reducers/timelines'; import { makeGetTimeline } from '../../../selectors';
import { deleteStatus } from '../../../actions/statuses'; import { deleteStatus } from '../../../actions/statuses';
const mapStateToProps = function (state, props) { const makeMapStateToProps = () => {
return { const getTimeline = makeGetTimeline();
statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)),
const mapStateToProps = (state, props) => ({
statuses: getTimeline(state, props.type),
me: state.getIn(['timelines', 'me']) me: state.getIn(['timelines', 'me'])
}; });
return mapStateToProps;
}; };
const mapDispatchToProps = function (dispatch, props) { const mapDispatchToProps = function (dispatch, props) {
@ -50,4 +54,4 @@ const mapDispatchToProps = function (dispatch, props) {
}; };
}; };
export default connect(mapStateToProps, mapDispatchToProps)(StatusList); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);

View file

@ -40,32 +40,6 @@ const initialState = Immutable.Map({
relationships: Immutable.Map() relationships: Immutable.Map()
}); });
export function selectStatus(state, id) {
let status = state.getIn(['timelines', 'statuses', id], null);
if (status === null) {
return null;
}
status = status.set('account', selectAccount(state, status.get('account')));
if (status.get('reblog') !== null) {
status = status.set('reblog', selectStatus(state, status.get('reblog')));
}
return status;
};
export function selectAccount(state, id) {
let account = state.getIn(['timelines', 'accounts', id], null);
if (account === null) {
return null;
}
return account.set('relationship', state.getIn(['timelines', 'relationships', id]));
};
function normalizeStatus(state, status) { function normalizeStatus(state, status) {
// Separate account // Separate account
let account = status.get('account'); let account = status.get('account');

View file

@ -0,0 +1,81 @@
import { createSelector } from 'reselect'
import Immutable from 'immutable';
const getStatuses = state => state.getIn(['timelines', 'statuses']);
const getAccounts = state => state.getIn(['timelines', 'accounts']);
const getAccountBase = (state, id) => state.getIn(['timelines', 'accounts', id], null);
const getAccountRelationship = (state, id) => state.getIn(['timelines', 'relationships', id]);
export const getAccount = createSelector([getAccountBase, getAccountRelationship], (base, relationship) => {
if (base === null) {
return null;
}
return base.set('relationship', relationship);
});
const getStatusBase = (state, id) => state.getIn(['timelines', 'statuses', id], null);
export const getStatus = createSelector([getStatusBase, getStatuses, getAccounts], (base, statuses, accounts) => {
if (base === null) {
return null;
}
return assembleStatus(base.get('id'), statuses, accounts);
});
const getAccountTimelineIds = (state, id) => state.getIn(['timelines', 'accounts_timelines', id], Immutable.List());
const assembleStatus = (id, statuses, accounts) => {
let status = statuses.get(id);
if (status === null) {
return null;
}
let reblog = statuses.get(status.get('reblog'), null);
if (reblog !== null) {
reblog = reblog.set('account', accounts.get(reblog.get('account')));
}
return status.set('reblog', reblog).set('account', accounts.get(status.get('account')));
};
const assembleStatusList = (ids, statuses, accounts) => {
return ids.map(statusId => assembleStatus(statusId, statuses, accounts)).filterNot(status => status === null);
};
export const getAccountTimeline = createSelector([getAccountTimelineIds, getStatuses, getAccounts], assembleStatusList);
const getTimelineIds = (state, timelineType) => state.getIn(['timelines', timelineType]);
export const makeGetTimeline = () => {
return createSelector([getTimelineIds, getStatuses, getAccounts], assembleStatusList);
};
const getStatusAncestorsIds = (state, id) => state.getIn(['timelines', 'ancestors', id], Immutable.OrderedSet());
export const getStatusAncestors = createSelector([getStatusAncestorsIds, getStatuses, getAccounts], assembleStatusList);
const getStatusDescendantsIds = (state, id) => state.getIn(['timelines', 'descendants', id], Immutable.OrderedSet());
export const getStatusDescendants = createSelector([getStatusDescendantsIds, getStatuses, getAccounts], assembleStatusList);
const getNotificationsBase = state => state.get('notifications');
export const getNotifications = createSelector([getNotificationsBase], (base) => {
let arr = [];
base.forEach(item => {
arr.push({
message: item.get('message'),
title: item.get('title'),
key: item.get('key'),
dismissAfter: 5000
});
});
return arr;
});

View file

@ -198,7 +198,7 @@
font-size: 13px; font-size: 13px;
display: block; display: block;
padding: 6px 16px; padding: 6px 16px;
width: 120px; width: 100px;
text-decoration: none; text-decoration: none;
background: #d9e1e8; background: #d9e1e8;
color: #282c37; color: #282c37;

View file

@ -17,15 +17,17 @@
"es6-promise": "^3.2.1", "es6-promise": "^3.2.1",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"moment": "^2.14.1", "moment": "^2.14.1",
"react-addons-perf": "^15.3.2",
"react-addons-pure-render-mixin": "^15.3.1", "react-addons-pure-render-mixin": "^15.3.1",
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-notification": "^6.1.1", "react-notification": "^6.1.1",
"react-redux": "^4.4.5", "react-redux": "^5.0.0-beta.3",
"react-redux-loading-bar": "^2.3.3", "react-redux-loading-bar": "^2.3.3",
"react-router": "^2.8.0", "react-router": "^2.8.0",
"react-simple-dropdown": "^1.1.4", "react-simple-dropdown": "^1.1.4",
"redux": "^3.5.2", "redux": "^3.5.2",
"redux-immutable": "^3.0.8", "redux-immutable": "^3.0.8",
"redux-thunk": "^2.1.0" "redux-thunk": "^2.1.0",
"reselect": "^2.5.4"
} }
} }