Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 65 additions & 34 deletions src/hocs/withData/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
PAGINATION
} from './retrievers';

const TRUE_FN = () => true;
const NULL_FN = () => null;
const EMPTY_OBJ_FN = () => {};

class Container extends Component {
constructor(props) {
super(props);
Expand All @@ -14,7 +18,8 @@ class Container extends Component {
this.resolvedDataTargetSize = 0;

this.timeouts = {
pendingScheduled: null
pendingScheduled: null,
minimumPendingTime: null
};

this.retrievers = {};
Expand All @@ -40,17 +45,17 @@ class Container extends Component {

componentWillMount() {
this.setupRetrievers(this.props);
this.trigger({});
this.trigger(this.props, true);
}

componentWillReceiveProps(newProps) {
const { shouldRefetch = (() => true) } = newProps;
if (!shouldRefetch(this.props.originalProps, newProps.originalProps)) {
componentWillReceiveProps(nextProps) {
const { shouldRefetch = TRUE_FN } = nextProps;
if (!shouldRefetch(this.props.originalProps, nextProps.originalProps)) {
return;
}
this.destroy();
this.setupRetrievers(newProps);
this.trigger(newProps.delays);
this.setupRetrievers(nextProps);
this.trigger(nextProps, false);
}

componentWillUnmount() {
Expand All @@ -71,31 +76,58 @@ class Container extends Component {
addResolvedData(field, data) {
this.resolvedData[field] = data;
if (this.resolvedDataTargetSize === Object.keys(this.resolvedData).length) {
this.clearPendingTimeout();
this.publish();
}
}

withMinimumPendingTime(fn) {
const { minimumPendingTime } = this.props.delays(this.props.originalProps);
if (this.state.pending && minimumPendingTime) {
this.setTimeout('minimumPendingTime', () => {
this.clearTimeout('minimumPendingTime');
fn();
}, minimumPendingTime);
} else {
fn();
}
}

publish() {
this.withMinimumPendingTime(() => {
this.clearTimeout('pendingScheduled');
this.rerender = true;
this.safeSetState({
pending: false,
pendingScheduled: false,
resolvedProps: { ...this.resolvedData },
error: null
});
}
});
}

setError(field, error) {
this.clearPendingTimeout();
this.safeSetState({
pending: false,
pendingScheduled: false,
error
this.withMinimumPendingTime(() => {
this.clearTimeout('pendingScheduled');
this.safeSetState({
pending: false,
error
});
});
}

clearPendingTimeout() {
hasTimeout(type) {
return !!this.timeouts[type];
}

setTimeout(type, ...args) {
this.clearTimeout(type);
this.timeouts[type] = setTimeout(...args);
}

clearTimeout(type) {
const { timeouts } = this;
if (timeouts.pendingScheduled) {
clearTimeout(timeouts.pendingScheduled);
timeouts.pendingScheduled = null;
if (timeouts[type]) {
clearTimeout(timeouts[type]);
timeouts[type] = null;
}
}

Expand Down Expand Up @@ -144,29 +176,28 @@ class Container extends Component {
publishError: publishError(key),
getProps,
getter: poll[key].resolve,
interval: (poll[key].interval || (() => null))(originalProps)
interval: (poll[key].interval || NULL_FN)(originalProps)
});
});

this.resolvedDataTargetSize = resolveKeys.length + observeKeys.length + pollKeys.length;
}

trigger(delays) {
trigger(props, firstRender) {
this.rerender = false;
const update = () => {
this.rerender = true;
this.resolvedData = {};
this.safeSetState({ pending: true, pendingScheduled: false, error: null });
};
if (delays.refetch) {
const { timeouts } = this;
timeouts.pendingScheduled = setTimeout(() => {
if (timeouts.pendingScheduled) {
const delays = props.delays(props.originalProps);
if (!firstRender && delays.refetch) {
this.setTimeout('pendingScheduled', () => {
if (this.hasTimeout('pendingScheduled')) {
update();
this.clearPendingTimeout();
this.clearTimeout('pendingScheduled');
}
}, delays.refetch);
this.safeSetState({ pendingScheduled: true });
} else {
update();
}
Expand Down Expand Up @@ -197,16 +228,21 @@ class Container extends Component {
}

const DEFAULT_DELAYS = {
refetch: 0
refetch: 0,
minimumPendingTime: 0
};

export function withData(conf) {
return component => {
const delays = (props) => ({
...DEFAULT_DELAYS,
...(conf.delays || EMPTY_OBJ_FN)(props)
});
class WithDataWrapper extends PureComponent {
render() {
const props = {
...conf,
delays: conf.delays || DEFAULT_DELAYS,
delays,
originalProps: this.props,
component
};
Expand All @@ -217,10 +253,5 @@ export function withData(conf) {
};
}

// wait with refetch spinner
// wait with initial spinner
//
// minimum time for spinner

withData.PAGINATION = PAGINATION;

55 changes: 49 additions & 6 deletions src/hocs/withData/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { withData } from '.';

const wait = (t = 1) => new Promise(res => setTimeout(() => res(), t));

const getTimeDiff = (l1, l2) => l2.t - l1.t;

const peter = { id: 'peter', name: 'peter' };
const gernot = { id: 'gernot', name: 'gernot' };
const robin = { id: 'robin', name: 'robin' };
Expand Down Expand Up @@ -239,9 +241,9 @@ describe('withData', () => {
user: ({ userId }) => api.user.getUser(userId)
},
pendingComponent: pendingSpy,
delays: {
delays: () => ({
refetch: 0
}
})
})(spy);

let stateContainer = null;
Expand Down Expand Up @@ -278,9 +280,9 @@ describe('withData', () => {
user: ({ userId }) => api.user.getUser(userId)
},
pendingComponent: pendingSpy,
delays: {
delays: () => ({
refetch: 100
}
})
})(spy);

let stateContainer = null;
Expand All @@ -306,9 +308,9 @@ describe('withData', () => {
resolve: {
user: ({ userId }) => api.user.getUser(userId)
},
delays: {
delays: () => ({
refetch: 100
}
})
})(spy);

let stateContainer = null;
Expand All @@ -327,6 +329,47 @@ describe('withData', () => {
});
});
});

it('allows to specify a mininumPendingTime to reduce flicker', () => {
const minimumPendingTime = 5;
const minimumPendingTimeWithThreshold = minimumPendingTime + 2;
const api = build(createConfig());
const { spy } = createSpyComponent();
const { spy: pendingSpy, logger: pendingLogger } = createSpyComponent();
const comp = withData({
resolve: {
user: ({ userId }) => api.user.getUser(userId)
},
pendingComponent: pendingSpy,
delays: () => ({
refetch: 0,
minimumPendingTime
})
})(spy);

let stateContainer = null;

render(comp, { userId: 'peter' }, c => { stateContainer = c; }, ({ userId }) => ({ userId }));

// the first render ideally shouldn't wait, if the promise resolves
// immediately
return wait(minimumPendingTimeWithThreshold).then(() => {
stateContainer.setState({ userId: 'gernot' });
return wait(minimumPendingTimeWithThreshold).then(() => {
pendingLogger.expectRenderCount(2);
const firstMount = pendingLogger.getByType('componentWillMount')[0];
const firstUnmount = pendingLogger.getByType('componentWillUnmount')[0];
const secondMount = pendingLogger.getByType('componentWillMount')[1];
const secondUnmount = pendingLogger.getByType('componentWillUnmount')[1];

const t1 = getTimeDiff(firstMount, firstUnmount);
const t2 = getTimeDiff(secondMount, secondUnmount);

expect(t1).to.be.at.least(minimumPendingTime);
expect(t2).to.be.at.least(minimumPendingTime);
});
});
});
});

describe('pagination', () => {
Expand Down