Skip to content
This repository has been archived by the owner on Jul 8, 2023. It is now read-only.

Commit

Permalink
➕ add safe-timers
Browse files Browse the repository at this point in the history
  • Loading branch information
deepsweet authored and Kir Belevich committed Aug 11, 2017
1 parent b783bfa commit c9a1acd
Show file tree
Hide file tree
Showing 15 changed files with 695 additions and 0 deletions.
18 changes: 18 additions & 0 deletions packages/safe-timers/demo/Target.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable no-console */
import React from 'react';
import { compose, withHandlers } from 'recompose';

import { withSafeTimeout } from '../src/';

const sayHi = () => console.log('Hi!');

const Target = ({ onButtonClick }) => (
<button onClick={onButtonClick}>Start 2 secs timeout</button>
);

export default compose(
withSafeTimeout,
withHandlers({
onButtonClick: ({ setSafeTimeout }) => () => setSafeTimeout(sayHi, 2000)
})
)(Target);
18 changes: 18 additions & 0 deletions packages/safe-timers/demo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

import Target from './Target';

const Demo = ({ targetKey, onButtonClick }) => (
<div>
<Target key={targetKey}/>
<button onClick={onButtonClick}>Remount</button>
</div>
);

export default compose(
withState('targetKey', 'setTargetKey', 0),
withHandlers({
onButtonClick: ({ setTargetKey, targetKey }) => () => setTargetKey(targetKey + 1)
})
)(Demo);
35 changes: 35 additions & 0 deletions packages/safe-timers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@hocs/safe-timers",
"library": "safeTimers",
"version": "0.1.0",
"description": "Safe timers HOCs for React",
"keywords": [
"react",
"hoc",
"recompose",
"timeout",
"interval",
"animationframe",
"idle"
],
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"dist/",
"es/",
"lib/"
],
"repository": "deepsweet/hocs",
"author": "Kir Belevich <[email protected]> (https://github.com/deepsweet)",
"license": {
"type": "MIT",
"url": "https://github.com/deepsweet/hocs/blob/master/license.md"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^15.6.1",
"recompose": "^0.24.0"
}
}
107 changes: 107 additions & 0 deletions packages/safe-timers/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# :watch: safe-timers

[![npm](https://img.shields.io/npm/v/@hocs/safe-timers.svg?style=flat-square)](https://www.npmjs.com/package/@hocs/safe-timers) [![ci](https://img.shields.io/travis/deepsweet/hocs/master.svg?style=flat-square)](https://travis-ci.org/deepsweet/hocs) [![coverage](https://img.shields.io/codecov/c/github/deepsweet/hocs/master.svg?style=flat-square)](https://codecov.io/github/deepsweet/hocs) [![deps](https://david-dm.org/deepsweet/hocs.svg?path=packages/safe-timers&style=flat-square)](https://david-dm.org/deepsweet/hocs?path=packages/safe-timers)

Part of a [collection](https://github.com/deepsweet/hocs) of Higher-Order Components for React, especially useful with [Recompose](https://github.com/acdlite/recompose).

Provides safe versions of `setTimeout`, `setInterval`, `requestAnimationFrame` and `requestIdleCallback` which will be cleared/cancelled automatically before component is unmounted.

Inspired by [react-timer-mixin](https://github.com/reactjs/react-timer-mixin).

## Install

```
yarn add recompose @hocs/safe-timers
```

## Usage

```js
withSafeTimeout: HigherOrderComponent
withSafeInterval: HigherOrderComponent
withSafeAnimationFrame: HigherOrderComponent
withSafeIdleCallback: HigherOrderComponent
```

Basic wrapper to remount Target component when we want:

```js
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

import Target from './Target';

const Demo = ({ targetKey, onButtonClick }) => (
<div>
<Target key={targetKey}/>
<button onClick={onButtonClick}>Remount</button>
</div>
);

export default compose(
withState('targetKey', 'setTargetKey', 0),
withHandlers({
onButtonClick: ({ setTargetKey, targetKey }) => () => setTargetKey(targetKey + 1)
})
)(Demo);
```

Target component which is using timeouts:

```js
import React from 'react';
import { compose, withHandlers } from 'recompose';
import { withSafeTimeout } from '@hocs/safe-timers';

const sayHi = () => console.log('Hi!');

const Target = ({ onButtonClick }) => (
<button onClick={onButtonClick}>Start 2 secs timeout</button>
);

export default compose(
withSafeTimeout,
withHandlers({
onButtonClick: ({ setSafeTimeout }) => () => setSafeTimeout(sayHi, 2000)
})
)(Target);
```

:tv: [Check out live demo](https://www.webpackbin.com/bins/-KrGkap1tBpYQzRPVr_-).

The same approach goes for all HOCs in this package:

* `withSafeTimeout` provides `setSafeTimeout` prop
* `withSafeInterval` provides `setSafeInterval` prop
* `withSafeAnimationFrame` provides `requestSafeAnimationFrame` prop
* `withSafeIdleCallback` provides `requestSafeIdleCallback` prop

So basically all you need to do in comparison with native timers is to add `Safe` word.

### Clear / Cancel

In order to keep your props as clean as possible, to manually clear/cancel a safe timer its "unsubscriber" is provided as a result of that timer call:

```js
const clearSafeInterval = setSafeInterval(() => {}, 100);

clearSafeInterval();
```

(How this pattern is called? In opposite to returning some unique `id`).

## Notes

### `requestSafeAnimationFrame`

You might still need a [polyfill](https://github.com/chrisdickinson/raf) ([Can I use?](https://caniuse.com/#feat=requestanimationframe)).

### `requestSafeIdleCallback`

You might still need a [polyfill](https://github.com/aFarkas/requestIdleCallback) ([Can I use?](https://caniuse.com/#feat=requestidlecallback)).

### `setImmediate`

[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate):

> This method is not expected to become standard, and is only implemented by recent builds of Internet Explorer and Node.js 0.10+. It meets resistance both from Gecko (Firefox) and Webkit (Google/Apple).
4 changes: 4 additions & 0 deletions packages/safe-timers/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as withSafeTimeout } from './withSafeTimeout';
export { default as withSafeInterval } from './withSafeInterval';
export { default as withSafeAnimationFrame } from './withSafeAnimationFrame';
export { default as withSafeIdleCallback } from './withSafeIdleCallback';
47 changes: 47 additions & 0 deletions packages/safe-timers/src/safeTimerFactory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { Component } from 'react';
import { setDisplayName, wrapDisplayName } from 'recompose';

const safeTimerFactory = (setFn, clearFn, propName, hocName) => (Target) => {
class SafeTimer extends Component {
constructor(props, context) {
super(props, context);

this.unsubscribers = [];
this[propName] = this[propName].bind(this);
}

componentWillUnmount() {
this.unsubscribers.forEach((unsubscribe) => unsubscribe());

this.unsubscribers = [];
}

[propName](...args) {
const id = setFn(...args);
const unsubscriber = () => clearFn(id);

this.unsubscribers.push(unsubscriber);

return unsubscriber;
}

render() {
const props = {
...this.props,
[propName]: this[propName]
};

return (
<Target {...props}/>
);
}
}

if (process.env.NODE_ENV !== 'production') {
return setDisplayName(wrapDisplayName(Target, hocName))(SafeTimer);
}

return SafeTimer;
};

export default safeTimerFactory;
8 changes: 8 additions & 0 deletions packages/safe-timers/src/withSafeAnimationFrame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import safeTimerFactory from './safeTimerFactory';

export default safeTimerFactory(
global.requestAnimationFrame,
global.cancelAnimationFrame,
'requestSafeAnimationFrame',
'withSafeAnimationFrame'
);
8 changes: 8 additions & 0 deletions packages/safe-timers/src/withSafeIdleCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import safeTimerFactory from './safeTimerFactory';

export default safeTimerFactory(
global.requestIdleCallback,
global.cancelIdleCallback,
'requestSafeIdleCallback',
'withSafeIdleCallback'
);
8 changes: 8 additions & 0 deletions packages/safe-timers/src/withSafeInterval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import safeTimerFactory from './safeTimerFactory';

export default safeTimerFactory(
global.setInterval,
global.clearInterval,
'setSafeInterval',
'withSafeInterval'
);
8 changes: 8 additions & 0 deletions packages/safe-timers/src/withSafeTimeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import safeTimerFactory from './safeTimerFactory';

export default safeTimerFactory(
global.setTimeout,
global.clearTimeout,
'setSafeTimeout',
'withSafeTimeout'
);
108 changes: 108 additions & 0 deletions packages/safe-timers/test/withSafeAnimationFrame.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-disable max-len */
import React from 'react';
import { mount } from 'enzyme';

const Target = () => null;

describe('withSafeAnimationFrame', () => {
let origRequestAnimationFrame = null;
let origCancelAnimationFrame = null;
let withSafeAnimationFrame = null;

beforeAll(() => {
origRequestAnimationFrame = global.requestAnimationFrame;
origCancelAnimationFrame = global.cancelAnimationFrame;

jest.resetModules();

global.requestAnimationFrame = jest.fn(() => 'id');
global.cancelAnimationFrame = jest.fn();

withSafeAnimationFrame = require('../src').withSafeAnimationFrame;
});

afterEach(() => {
global.requestAnimationFrame.mockClear();
global.cancelAnimationFrame.mockClear();
});

afterAll(() => {
global.requestAnimationFrame = origRequestAnimationFrame;
global.cancelAnimationFrame = origCancelAnimationFrame;
});

it('should pass props through', () => {
const EnchancedTarget = withSafeAnimationFrame(Target);
const wrapper = mount(
<EnchancedTarget a={1} b={2}/>
);
const target = wrapper.find(Target);

expect(target.prop('a')).toBe(1);
expect(target.prop('b')).toBe(2);
});

it('should provide `requestSafeAnimationFrame` prop and unsubscriber as its call return', () => {
const callback = () => {};
const EnchancedTarget = withSafeAnimationFrame(Target);
const wrapper = mount(
<EnchancedTarget/>
);
const cancelSafeAnimationFrame = wrapper.find(Target).prop('requestSafeAnimationFrame')(callback, 'a', 'b');

expect(global.requestAnimationFrame).toHaveBeenCalledTimes(1);
expect(global.requestAnimationFrame).toHaveBeenCalledWith(callback, 'a', 'b');

cancelSafeAnimationFrame();

expect(global.cancelAnimationFrame).toHaveBeenCalledTimes(1);
expect(global.cancelAnimationFrame).toHaveBeenCalledWith('id');
});

it('should clear all safe intervals on unmount', () => {
const EnchancedTarget = withSafeAnimationFrame(Target);
const wrapper = mount(
<EnchancedTarget/>
);
const requestSafeAnimationFrame = wrapper.find(Target).prop('requestSafeAnimationFrame');

requestSafeAnimationFrame();
requestSafeAnimationFrame();
requestSafeAnimationFrame();

wrapper.unmount();

expect(global.cancelAnimationFrame).toHaveBeenCalledTimes(3);
expect(global.cancelAnimationFrame).toHaveBeenCalledWith('id');
});

describe('display name', () => {
const origNodeEnv = process.env.NODE_ENV;

afterAll(() => {
process.env.NODE_ENV = origNodeEnv;
});

it('should wrap display name in non-production env', () => {
process.env.NODE_ENV = 'test';

const EnchancedTarget = withSafeAnimationFrame(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(wrapper.name()).toBe('withSafeAnimationFrame(Target)');
});

it('should not wrap display name in production env', () => {
process.env.NODE_ENV = 'production';

const EnchancedTarget = withSafeAnimationFrame(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(wrapper.name()).toBe('SafeTimer');
});
});
});
Loading

0 comments on commit c9a1acd

Please sign in to comment.