Browse Source

Use reselect to memoize denormalization in UI state

Also upgrade react-redux to latest version. This is a performance update
closed-social-v3
Eugen Rochko 8 years ago
parent
commit
ef9d4f4e06
12 changed files with 136 additions and 80 deletions
  1. +1
    -0
      app/assets/javascripts/components.js
  2. +1
    -1
      app/assets/javascripts/components/components/status_list.jsx
  3. +5
    -9
      app/assets/javascripts/components/features/account/index.jsx
  4. +21
    -19
      app/assets/javascripts/components/features/public_timeline/index.jsx
  5. +8
    -8
      app/assets/javascripts/components/features/status/index.jsx
  6. +2
    -2
      app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx
  7. +2
    -6
      app/assets/javascripts/components/features/ui/containers/notifications_container.jsx
  8. +10
    -6
      app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
  9. +0
    -26
      app/assets/javascripts/components/reducers/timelines.jsx
  10. +81
    -0
      app/assets/javascripts/components/selectors/index.jsx
  11. +1
    -1
      app/assets/stylesheets/components.scss
  12. +4
    -2
      package.json

+ 1
- 0
app/assets/javascripts/components.js View File

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

+ 1
- 1
app/assets/javascripts/components/components/status_list.jsx View File

@ -28,7 +28,7 @@ const StatusList = React.createClass({
const { statuses, onScrollToBottom, ...other } = this.props;
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>
{statuses.map((status) => {
return <Status key={status.get('id')} {...other} status={status} />;

+ 5
- 9
app/assets/javascripts/components/features/account/index.jsx View File

@ -20,22 +20,18 @@ import {
} from '../../actions/interactions';
import Header from './components/header';
import {
selectStatus,
selectAccount
} from '../../reducers/timelines';
getAccountTimeline,
getAccount
} from '../../selectors';
import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator';
import Immutable from 'immutable';
import ActionBar from './components/action_bar';
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) => ({
account: selectAccount(state, Number(props.params.accountId)),
statuses: selectStatuses(state, Number(props.params.accountId)),
account: getAccount(state, Number(props.params.accountId)),
statuses: getAccountTimeline(state, Number(props.params.accountId)),
me: state.getIn(['timelines', 'me'])
});

+ 21
- 19
app/assets/javascripts/components/features/public_timeline/index.jsx View File

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

+ 8
- 8
app/assets/javascripts/components/features/status/index.jsx View File

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

+ 2
- 2
app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx View File

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

+ 2
- 6
app/assets/javascripts/components/features/ui/containers/notifications_container.jsx View File

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

+ 10
- 6
app/assets/javascripts/components/features/ui/containers/status_list_container.jsx View File

@ -8,14 +8,18 @@ import {
unfavourite
} from '../../../actions/interactions';
import { expandTimeline } from '../../../actions/timelines';
import { selectStatus } from '../../../reducers/timelines';
import { makeGetTimeline } from '../../../selectors';
import { deleteStatus } from '../../../actions/statuses';
const mapStateToProps = function (state, props) {
return {
statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)),
const makeMapStateToProps = () => {
const getTimeline = makeGetTimeline();
const mapStateToProps = (state, props) => ({
statuses: getTimeline(state, props.type),
me: state.getIn(['timelines', 'me'])
};
});
return mapStateToProps;
};
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);

+ 0
- 26
app/assets/javascripts/components/reducers/timelines.jsx View File

@ -40,32 +40,6 @@ const initialState = 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) {
// Separate account
let account = status.get('account');

+ 81
- 0
app/assets/javascripts/components/selectors/index.jsx 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;
});

+ 1
- 1
app/assets/stylesheets/components.scss View File

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

+ 4
- 2
package.json View File

@ -17,15 +17,17 @@
"es6-promise": "^3.2.1",
"immutable": "^3.8.1",
"moment": "^2.14.1",
"react-addons-perf": "^15.3.2",
"react-addons-pure-render-mixin": "^15.3.1",
"react-immutable-proptypes": "^2.1.0",
"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-router": "^2.8.0",
"react-simple-dropdown": "^1.1.4",
"redux": "^3.5.2",
"redux-immutable": "^3.0.8",
"redux-thunk": "^2.1.0"
"redux-thunk": "^2.1.0",
"reselect": "^2.5.4"
}
}

Loading…
Cancel
Save