This repository has been archived by the owner on Jul 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
695 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.