Skip to content
Merged
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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# hoc-react-loader
[![CircleCI](https://circleci.com/gh/Zenika/hoc-react-loader.svg?&style=shield&circle-token=07eae4d9bdbe138c04d32753312ba543a4e08f34)](https://circleci.com/gh/Zenika/hoc-react-loader/tree/master) [![NPM Version](https://badge.fury.io/js/hoc-react-loader.svg)](https://www.npmjs.com/package/hoc-react-loader) [![Coverage Status](https://coveralls.io/repos/github/Zenika/hoc-react-loader/badge.svg?branch=master)](https://coveralls.io/github/Zenika/hoc-react-loader?branch=master)

This is a [higher order component](https://facebook.github.io/react/docs/higher-order-components.html) ("HOC"). It's an advanced pattern used in React that let you reuse code logic, it can be summarized as a component factory. It improves isolation, interoperability and maintainability of your code base.
This is a [higher order component](https://facebook.github.io/react/docs/higher-order-components.html) ("HOC"). It's an advanced pattern used in React that let you reuse code logic, it can be summarized as a component factory. It improves isolation, interoperability and maintainability of your code base.

**hoc-react-loader**'s purpose is to call a `load` callback passed through the `props` of a component only once (at `componentWillMount`). This is convenient to load data from a backend for instance. The component shows a loading indicator when it's waiting for the props to be defined. The loading indicator can be changed easily.

Expand Down Expand Up @@ -94,3 +94,16 @@ export default loader({ error: 'errorMessage' })(MyComponent)
// CustomErrorComponent will be displayed if 'error' prop is truthy
export default loader({ ErrorIndicator: CustomErrorComponent })(MyComponent)
```

### Delay parameter

When a component loads very quickly, you will see a flash of the loading component.
To avoid this behaviour, you can add a `delay` parameter to the loader with a time in milliseconds.
Then, the loading indicator will be rendered after the delay if the Component can't be rendered before that.

```js
// loading indicator will be displayed only after 200ms
export default loader({ print: ['data'], delay: 200 })(MyComponent)
```

By default, no delay is defined.
31 changes: 29 additions & 2 deletions build/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ exports.default = function () {
ErrorIndicator = _ref.ErrorIndicator,
print = _ref.print,
load = _ref.load,
error = _ref.error;
error = _ref.error,
delay = _ref.delay;

var loadFunctionName = isString(load) ? load : 'load';
var isLoadFunction = isFunction(load);
Expand Down Expand Up @@ -103,7 +104,8 @@ exports.default = function () {
}

return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref2 = _class.__proto__ || Object.getPrototypeOf(_class)).call.apply(_ref2, [this].concat(args))), _this), _this.state = {
props: {}
props: {},
print: true
}, _this.omitLoadInProps = function (props) {
var isLoadAFunction = isFunction(props[loadFunctionName]);

Expand All @@ -124,6 +126,8 @@ exports.default = function () {
_createClass(_class, [{
key: 'componentWillMount',
value: function componentWillMount() {
var _this2 = this;

// Load from hoc argument
if (isLoadFunction) {
load(this.props, this.context);
Expand All @@ -133,6 +137,25 @@ exports.default = function () {
if (this.omitLoadInProps(this.props)) {
this.props[loadFunctionName](this.props, this.context);
}

// set delay
if (delay) {
this.setState(function (state) {
return _extends({}, state, { print: false });
});
this.timer = setTimeout(function () {
return _this2.setState(function (state) {
return _extends({}, state, { print: true });
});
}, delay);
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
}, {
key: 'render',
Expand All @@ -145,6 +168,10 @@ exports.default = function () {
return _react2.default.createElement(ComposedComponent, this.state.props);
}

if (!this.state.print) {
return null;
}

return _react2.default.createElement(LoadingIndicator, this.state.props);
}
}]);
Expand Down
8 changes: 8 additions & 0 deletions src/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`hoc-react-loader delay parameter should render component when loaded when delay not past 1`] = `"<div class=\\"component\\">Component</div>"`;

exports[`hoc-react-loader delay parameter should render component when loaded when delay past 1`] = `"<div class=\\"component\\">Component</div>"`;

exports[`hoc-react-loader delay parameter should render loading indicator after delay and component not still loaded 1`] = `"<div>load</div>"`;

exports[`hoc-react-loader delay parameter should render nothing before delay and component not still loaded 1`] = `null`;

exports[`hoc-react-loader error parameter should print Component -error as a value- 1`] = `"<div class=\\"component\\">Component</div>"`;

exports[`hoc-react-loader error parameter should print Component -error as an array- 1`] = `"<div class=\\"component\\">Component</div>"`;
Expand Down
18 changes: 18 additions & 0 deletions src/core.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default (
print,
load,
error,
delay,
} = {},
) => {
const loadFunctionName = isString(load) ? load : 'load'
Expand All @@ -60,6 +61,7 @@ export default (

state = {
props: {},
print: true,
}

omitLoadInProps = (props) => {
Expand Down Expand Up @@ -89,6 +91,18 @@ export default (
if (this.omitLoadInProps(this.props)) {
this.props[loadFunctionName](this.props, this.context)
}

// set delay
if (delay) {
this.setState(state => ({ ...state, print: false }))
this.timer = setTimeout(() => this.setState(state => ({ ...state, print: true })), delay)
}
}

componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer)
}
}

componentWillReceiveProps = (nextProps) => {
Expand All @@ -104,6 +118,10 @@ export default (
return <ComposedComponent {...this.state.props} />
}

if (!this.state.print) {
return null
}

return <LoadingIndicator {...this.state.props} />
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,55 @@ describe('hoc-react-loader', () => {
expect(mounted.html()).toMatchSnapshot()
})
})

describe('delay parameter', () => {
jest.useFakeTimers()

it('should render nothing before delay and component not still loaded', () => {
const Wrapped = loader({ delay: 10000 })(Component)

const mounted = mount(<Wrapped loaded={false} />)
expect(mounted.html()).toMatchSnapshot()
})

it('should render loading indicator after delay and component not still loaded', () => {
const LoadingIndicator = () => <div>load</div>
const Wrapped = loader({ delay: 10000, LoadingIndicator })(Component)

const mounted = mount(<Wrapped loaded={false} />)
jest.runAllTimers()
expect(mounted.html()).toMatchSnapshot()
})

it('should render component when loaded when delay not past', () => {
const Wrapped = loader({ delay: 10000 })(Component)

const mounted = mount(<Wrapped some="props" />)
expect(mounted.html()).toMatchSnapshot()
})

it('should render component when loaded when delay past', () => {
const Wrapped = loader({ delay: 10000 })(Component)

const mounted = mount(<Wrapped some="props" />)
jest.runAllTimers()
expect(mounted.html()).toMatchSnapshot()
})

it('should unregister the timer when component unmounted', () => {
const Wrapped = loader({ delay: 10000 })(Component)

const mounted = mount(<Wrapped some="props" />)
mounted.unmount()
expect(clearTimeout).toHaveBeenCalledTimes(1)
})

it('should unmount correctly when no delay defined', () => {
const Wrapped = loader()(Component)

const mounted = mount(<Wrapped some="props" />)
mounted.unmount()
expect(mounted.html()).toBe(null)
})
})
})