Filter queryData in withFragments and optimize rendering

This commit is contained in:
Chi Vinh Le
2017-08-17 21:31:09 +07:00
parent 1ff6c5bdcc
commit 7ea6d133c2
11 changed files with 227 additions and 93 deletions
@@ -224,6 +224,10 @@ export default class Comment extends React.Component {
return;
}
commentPostedHandler = () => {
this.props.setActiveReplyBox('');
}
// getVisibileReplies returns a list containing comments
// which were authored by current user or comes before the `idCursor`.
getVisibileReplies() {
@@ -372,10 +376,13 @@ export default class Comment extends React.Component {
// props that are passed down the slots.
const slotProps = {
data,
depth,
};
const queryData = {
root,
asset,
comment,
depth,
};
return (
@@ -389,6 +396,7 @@ export default class Comment extends React.Component {
className={`${styles.commentAvatar} talk-stream-comment-avatar`}
fill="commentAvatar"
{...slotProps}
queryData={queryData}
inline
/>
@@ -411,6 +419,7 @@ export default class Comment extends React.Component {
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
queryData={queryData}
/>
{ isActive && (currentUser && (comment.user.id === currentUser.id)) &&
@@ -457,6 +466,7 @@ export default class Comment extends React.Component {
fill="commentContent"
defaultComponent={CommentContent}
{...slotProps}
queryData={queryData}
/>
</div>
}
@@ -468,6 +478,7 @@ export default class Comment extends React.Component {
<Slot
fill="commentReactions"
{...slotProps}
queryData={queryData}
inline
/>
{!disableReply &&
@@ -484,6 +495,7 @@ export default class Comment extends React.Component {
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
queryData={queryData}
inline
/>
<ActionButton>
@@ -509,9 +521,7 @@ export default class Comment extends React.Component {
{activeReplyBox === comment.id
? <ReplyBox
commentPostedHandler={() => {
setActiveReplyBox('');
}}
commentPostedHandler={this.commentPostedHandler}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
setActiveReplyBox={setActiveReplyBox}
@@ -40,6 +40,15 @@ class Stream extends React.Component {
}
}
commentIsIgnored = (comment) => {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
render() {
const {
data,
@@ -48,7 +57,7 @@ class Stream extends React.Component {
setActiveReplyBox,
appendItemArray,
commentClassNames,
root: {asset, asset: {comment, comments, totalCommentCount}, me},
root: {asset, asset: {comment, comments, totalCommentCount}},
postComment,
addNotification,
editComment,
@@ -89,16 +98,9 @@ class Stream extends React.Component {
user.suspension.until &&
new Date(user.suspension.until) > new Date();
const commentIsIgnored = (comment) => {
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = {data, root, asset};
const slotProps = {data};
const slotQueryData = {root, asset};
if (!comment && !comments) {
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
@@ -160,6 +162,7 @@ class Stream extends React.Component {
<Slot
fill="stream"
queryData={slotQueryData}
{...slotProps}
/>
@@ -194,7 +197,7 @@ class Stream extends React.Component {
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={highlightedComment.id}
commentIsIgnored={commentIsIgnored}
commentIsIgnored={this.commentIsIgnored}
comment={highlightedComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
@@ -209,6 +212,7 @@ class Stream extends React.Component {
>
<Slot
fill="streamFilter"
queryData={slotQueryData}
{...slotProps}
/>
</div>
@@ -219,6 +223,7 @@ class Stream extends React.Component {
tabSlot={'streamTabs'}
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
queryData={slotQueryData}
appendTabs={
<Tab tabId={'all'} key='all'>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
@@ -245,7 +250,7 @@ class Stream extends React.Component {
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
@@ -3,7 +3,9 @@ import React from 'react';
import Comment from '../components/Comment';
import {withFragments} from 'coral-framework/hocs';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import {THREADING_LEVEL} from '../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
import {nest} from '../graphql/utils';
const slots = [
'streamQuestionArea',
@@ -48,6 +50,36 @@ const withAnimateEnter = hoistStatics((BaseComponent) => {
return WithAnimateEnter;
});
const singleCommentFragment = gql`
fragment CoralEmbedStream_Comment_SingleComment on Comment {
id
body
created_at
status
replyCount
tags {
tag {
name
}
}
user {
id
username
}
action_summaries {
__typename
count
current_user {
id
}
}
editing {
edited
editableUntil
}
}
`;
const withCommentFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
@@ -63,33 +95,21 @@ const withCommentFragments = withFragments({
`,
comment: gql`
fragment CoralEmbedStream_Comment_comment on Comment {
id
body
created_at
status
replyCount
tags {
tag {
name
...CoralEmbedStream_Comment_SingleComment
${nest(`
replies(limit: 3, excludeIgnored: $excludeIgnored) {
nodes {
...CoralEmbedStream_Comment_SingleComment
...nest
}
hasNextPage
startCursor
endCursor
}
}
user {
id
username
}
action_summaries {
__typename
count
current_user {
id
}
}
editing {
edited
editableUntil
}
`, THREADING_LEVEL)}
${getSlotFragmentSpreads(slots, 'comment')}
}
${singleCommentFragment}
`
});
@@ -161,19 +161,11 @@ class StreamContainer extends React.Component {
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
status
user {
id
}
...${getDefinitionName(Comment.fragments.comment)}
${nest(`
replies(excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
...nest
}
hasNextPage
startCursor
endCursor
}
`, THREADING_LEVEL)}
}
${Comment.fragments.comment}
`;
@@ -210,27 +202,14 @@ const LOAD_MORE_QUERY = gql`
query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
${nest(`
replies(limit: 3, excludeIgnored: $excludeIgnored) {
nodes {
id
...${getDefinitionName(Comment.fragments.comment)}
...nest
}
hasNextPage
startCursor
endCursor
}
`, THREADING_LEVEL)}
...CoralEmbedStream_Stream_comment
}
hasNextPage
startCursor
endCursor
}
}
${Comment.fragments.comment}
${commentFragment}
`;
const slots = [
@@ -2,7 +2,7 @@ import React from 'react';
import StreamTabPanel from '../components/StreamTabPanel';
import {connect} from 'react-redux';
import omit from 'lodash/omit';
import {getSlotComponents} from 'coral-framework/helpers/plugins';
import {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins';
import {Tab, TabPane} from 'coral-ui';
import {getShallowChanges} from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
@@ -43,14 +43,14 @@ class StreamTabPanelContainer extends React.Component {
}
getSlotComponents(slot, props = this.props) {
return getSlotComponents(slot, props.reduxState, props.slotProps);
return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
}
getPluginTabElements(props = this.props) {
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => (
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...props.slotProps}
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
active={this.props.activeTab === PluginComponent.talkPluginName}
/>
</Tab>
@@ -61,7 +61,7 @@ class StreamTabPanelContainer extends React.Component {
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => (
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
<PluginComponent
{...props.slotProps}
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
/>
</TabPane>
));
@@ -95,6 +95,8 @@ StreamTabPanelContainer.propTypes = {
fallbackTab: PropTypes.string.isRequired,
tabSlot: PropTypes.string.isRequired,
tabPaneSlot: PropTypes.string.isRequired,
slotProps: PropTypes.object.isRequired,
queryData: PropTypes.object,
className: PropTypes.string,
sub: PropTypes.bool,
};
+10 -7
View File
@@ -2,11 +2,13 @@ import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import {getShallowChanges} from 'coral-framework/utils';
const emptyConfig = {};
class Slot extends React.Component {
shouldComponentUpdate(next) {
@@ -23,20 +25,20 @@ class Slot extends React.Component {
return changes.length !== 0;
}
getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, ...rest} = this.props) {
getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) {
return rest;
}
getChildren(props = this.props) {
return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props));
return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData);
}
render() {
const {inline = false, className, reduxState, defaultComponent: DefaultComponent} = this.props;
const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
let children = this.getChildren();
const pluginConfig = reduxState.config.pluginConfig || {};
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
if (children.length === 0 && DefaultComponent) {
children = <DefaultComponent {...this.getSlotProps(this.props)} />;
children = <DefaultComponent {...getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData)} />;
}
return (
@@ -48,7 +50,8 @@ class Slot extends React.Component {
}
Slot.propTypes = {
fill: React.PropTypes.string.isRequired
fill: React.PropTypes.string.isRequired,
queryData: React.PropTypes.object,
};
const mapStateToProps = (state) => ({
+30 -9
View File
@@ -11,8 +11,11 @@ import camelize from './camelize';
import plugins from 'pluginsConfig';
import uuid from 'uuid/v4';
export function getSlotComponents(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.plugin_config || {};
// This is returned for pluginConfig when it is empty.
const emptyConfig = {};
export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
return flatten(plugins
// Filter out components that have slots and have been disabled in `plugin_config`
@@ -25,7 +28,7 @@ export function getSlotComponents(slot, reduxState, props = {}) {
if(!component.isExcluded) {
return true;
}
let resolvedProps = {...props, config: pluginConfig};
let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData);
if (component.mapStateToProps) {
resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)};
}
@@ -33,17 +36,35 @@ export function getSlotComponents(slot, reduxState, props = {}) {
});
}
export function isSlotEmpty(slot, reduxState, props) {
return getSlotComponents(slot, reduxState, props).length === 0;
export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
return getSlotComponents(slot, reduxState, props, queryData).length === 0;
}
/**
* getSlotComponentProps calculate the props we would pass to the slot component.
* query datas are only passed to the component if it is defined in `component.fragments`.
*/
export function getSlotComponentProps(component, reduxState, props, queryData) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
return {
...props,
config: pluginConfig,
...(
component.fragments
? pick(queryData, Object.keys(component.fragments))
: queryData // TODO: should be {}
)
};
}
/**
* Returns React Elements for given slot.
*/
export function getSlotElements(slot, reduxState, props = {}) {
const pluginConfig = reduxState.config.plugin_config || {};
return getSlotComponents(slot, reduxState, props)
.map((component, i) => React.createElement(component, {key: i, ...props, config: pluginConfig}));
export function getSlotElements(slot, reduxState, props = {}, queryData = {}) {
return getSlotComponents(slot, reduxState, props, queryData)
.map((component, i) => {
return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)});
});
}
export function getSlotFragments(slot, part) {
+83 -2
View File
@@ -1,17 +1,98 @@
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
import React from 'react';
import graphql from 'graphql-anywhere';
import {resolveFragments} from 'coral-framework/services/graphqlRegistry';
import mapValues from 'lodash/mapValues';
import hoistStatics from 'recompose/hoistStatics';
import {getShallowChanges} from 'coral-framework/utils';
// TODO: Should not depend on `props.data`
// Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38
function filter(doc, data, variables) {
const resolver = (
fieldName,
root,
args,
context,
info,
) => {
return root[info.resultKey];
};
return graphql(resolver, doc, data, null, variables);
}
// filterProps returns only the property as defined in the fragments.
// TODO: Should not depend on `props.data`
function filterProps(props, fragments) {
const filtered = {};
Object.keys(fragments).forEach((key) => {
if (!(key in props)) {
return;
}
filtered[key] = filter(fragments[key], props[key], props.data.variables);
});
return filtered;
}
// hasEqualLeaves compares two different apollo query result for equality.
function hasEqualLeaves(a, b, path = '') {
for (const key in a) {
if (typeof a[key] === 'object') {
if (Array.isArray(a[key])) {
if (a[key].length !== b[key].length) {
return false;
}
}
if (!hasEqualLeaves(a[key], b[key], `${path}.${key}`)) {
return false;
}
continue;
}
if (a[key] !== b[key]) {
return false;
}
}
return true;
}
export default (fragments) => hoistStatics((BaseComponent) => {
class WithFragments extends React.Component {
fragments = mapValues(fragments, (val) => resolveFragments(val));
fragmentKeys = Object.keys(fragments).sort();
// Cache variables between lifecycles to speed up render.
filteredProps = filterProps(this.props, this.fragments)
queryDataHasChanged = false;
lastFilteredProps = null;
shallowChanges = null;
componentWillReceiveProps(next) {
this.shallowChanges = getShallowChanges(this.props, next);
this.queryDataHasChanged = this.fragmentKeys.some((key) => this.shallowChanges.indexOf(key) >= 0);
if (this.queryDataHasChanged) {
// If query data has changed, we compute the next filtered props.
this.lastFilteredProps = this.filteredProps;
this.filteredProps = filterProps(next, this.fragments);
}
}
shouldComponentUpdate(next) {
// If only query data was changed.
if (this.queryDataHasChanged && this.shallowChanges.every((key) => this.fragmentKeys.indexOf(key) >= 0)) {
return !hasEqualLeaves(this.lastFilteredProps, this.filteredProps);
}
return this.shallowChanges.length !== 0;
}
render() {
const queryProps = this.filteredProps;
return <BaseComponent
{...this.props}
{...queryProps}
/>;
}
}
+9 -2
View File
@@ -176,7 +176,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
createdSubscription = context.client.subscribe({
query: REACTION_CREATED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
assetId: this.props.asset.id,
},
}).subscribe({
next: this.onReactionCreated,
@@ -186,7 +186,7 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
deletedSubscription = context.client.subscribe({
query: REACTION_DELETED_SUBSCRIPTION,
variables: {
assetId: this.props.root.asset.id,
assetId: this.props.asset.id,
},
}).subscribe({
next: this.onReactionDeleted,
@@ -372,9 +372,16 @@ export default (reaction) => hoistStatics((WrappedComponent) => {
const enhance = compose(
withFragments({
asset: gql`
fragment ${Reaction}Button_asset on Asset {
id
}
`,
comment: gql`
fragment ${Reaction}Button_comment on Comment {
id
action_summaries {
__typename
... on ${Reaction}ActionSummary {
count
current_user {
+6
View File
@@ -93,8 +93,14 @@ export default (tag) => hoistStatics((WrappedComponent) => {
const enhance = compose(
withFragments({
asset: gql`
fragment ${Tag}Button_asset on Asset {
id
}
`,
comment: gql`
fragment ${Tag}Button_comment on Comment {
id
tags {
tag {
name
@@ -16,8 +16,8 @@ class TabPaneContainer extends React.Component {
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.asset.featuredComments.endCursor,
asset_id: this.props.root.asset.id,
cursor: this.props.asset.featuredComments.endCursor,
asset_id: this.props.asset.id,
sort: 'REVERSE_CHRONOLOGICAL',
excludeIgnored: this.props.data.variables.excludeIgnored,
},