diff --git a/package.json b/package.json
index e4335c8..969a00b 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
{
"name": "rax-hooks",
+ "private": true,
"description": "Contains rax hooks that are used very frequently",
"scripts": {
"start": "npm run clean && node ./scripts/compile-packages.js --watch",
@@ -20,6 +21,9 @@
"rax",
"hooks"
],
+ "workspaces": [
+ "packages/*"
+ ],
"config": {
"ghooks": {
"commit-msg": "./scripts/validate-commit-msg.js"
@@ -70,7 +74,7 @@
"lerna": "^3.19.0",
"minimatch": "^3.0.4",
"minimist": "^1.2.0",
- "rax": "^1.1.0",
+ "rax": "1.1.4",
"rax-test-renderer": "^1.0.0",
"semver": "^7.1.2",
"semver-regex": "^2.0.0",
diff --git a/packages/useCountDown/README.md b/packages/useCountDown/README.md
index 312464e..58ed796 100644
--- a/packages/useCountDown/README.md
+++ b/packages/useCountDown/README.md
@@ -2,7 +2,7 @@
-A countdown hooks which will return the left days/hours/minutes/seconds state.
+A countdown hooks which will return the left time(in millisecond).
## Install
@@ -12,23 +12,40 @@ $ npm install rax-use-countdown --save
## API
-The API will recevie two params -- `start`/`end`.
+The API will recevie two params -- `timeToCount`/`interval`.
| | Type | Description |
| ----- | -------- | ----------- |
-| start | `number` | Start time |
-| ent | `number` | End time |
+| timeToCount | `number` | total time to count, in millisecond |
+| interval | `number` | interval time on every tick, in millisecond |
+| events | `object` | onStart, onTick, onPause, onResume, onCompleted, onReset |
## Example
```jsx
-import { createElement } from 'rax';
+import { createElement, useEffect } from 'rax';
import useCountDown from 'rax-use-countdown';
function Example() {
- const now = Date.now();
- const { days, hours, minutes, seconds } = useCountDown(now, now - 10000000);
-
- return
There only left {days}days {hours}hours {minutes}minutes {seconds}seconds
;
+ // countdown 10s with 100ms interval
+ const [timeLeft, { start, pause, resume, reset }] = useCountDown(10 * 1000, 100, {
+ onStart() {},
+ onTick(timeLeft) {
+ // countdown tick, unit of `timeLeft` is millisecond
+ },
+ onPause() {},
+ onResume() {},
+ onCompleted() {
+ // countdown completed
+ },
+ onReset() {}
+ });
+
+ useEffect(() => {
+ start();
+ }, []);
+
+ // you can format timeLeft by yourself
+ return There only left { timeLeft } milliseconds
;
}
```
diff --git a/packages/useCountDown/package.json b/packages/useCountDown/package.json
index b362e28..2b11a11 100644
--- a/packages/useCountDown/package.json
+++ b/packages/useCountDown/package.json
@@ -1,6 +1,6 @@
{
"name": "rax-use-countdown",
- "version": "1.0.2",
+ "version": "2.0.0",
"description": "Rax useCountDown hook",
"license": "BSD-3-Clause",
"main": "lib/index.js",
diff --git a/packages/useCountDown/src/__tests__/useCountDown.js b/packages/useCountDown/src/__tests__/useCountDown.js
index bf0551f..1c83145 100644
--- a/packages/useCountDown/src/__tests__/useCountDown.js
+++ b/packages/useCountDown/src/__tests__/useCountDown.js
@@ -1,4 +1,4 @@
-import { createElement } from 'rax';
+import { createElement, useEffect } from 'rax';
import renderer from 'rax-test-renderer';
import useCountDown from '..';
@@ -13,21 +13,196 @@ function asyncFn(delay, val) {
describe('useCountDown', () => {
it('3 seconds count down', async() => {
function App() {
- const now = Date.now();
- const { days, hours, minutes, seconds } = useCountDown(now + 3000, now);
+ const [timeLeft, { start }] = useCountDown(3 * 1000, 1000);
- return {days}:{hours}:{minutes}:{seconds}
;
+ useEffect(() => {
+ start();
+ }, []);
+
+ return {timeLeft}
;
}
const tree = renderer.create();
// For render
await asyncFn(200);
+ expect(tree.toJSON().children.join('')).toEqual('3000');
+
+ await asyncFn(1000);
+ expect(tree.toJSON().children.join('')).toEqual('2000');
+
+ await asyncFn(2000);
+ expect(tree.toJSON().children.join('')).toEqual('0');
+ });
+
+ it('100ms interval', async() => {
+ function App() {
+ const [timeLeft, { start }] = useCountDown(1000, 100);
+
+ useEffect(() => {
+ start();
+ }, []);
+
+ return {timeLeft}
;
+ }
+
+ const tree = renderer.create();
+
+ // For render
+ await asyncFn(50);
+ expect(tree.toJSON().children.join('')).toEqual('1000');
+
+ await asyncFn(100);
+ expect(tree.toJSON().children.join('')).toEqual('900');
+
+ await asyncFn(100);
+ expect(tree.toJSON().children.join('')).toEqual('800');
+
+ await asyncFn(700);
+ expect(tree.toJSON().children.join('')).toEqual('100');
+
+ await asyncFn(100);
+ expect(tree.toJSON().children.join('')).toEqual('0');
+ });
+
+ it('pause', async() => {
+ const mockPause = jest.fn();
+ function App() {
+ const [timeLeft, { start, pause }] = useCountDown(1000, 100, {
+ onPause: mockPause,
+ });
+
+ useEffect(() => {
+ start();
+
+ setTimeout(() => pause(), 550);
+ }, []);
+
+ return {timeLeft}
;
+ }
+
+ const tree = renderer.create();
+
+ // For render
+ await asyncFn(150);
+ expect(tree.toJSON().children.join('')).toEqual('900');
+
+ await asyncFn(500);
+ expect(tree.toJSON().children.join('')).toEqual('500');
+ await asyncFn(500);
+ expect(tree.toJSON().children.join('')).toEqual('500');
+ expect(mockPause).toHaveBeenCalledTimes(1);
+ });
+
+ it('resume', async() => {
+ const mockPause = jest.fn();
+ const mockResume = jest.fn();
+ function App() {
+ const [timeLeft, { start, pause, resume }] = useCountDown(1000, 100, {
+ onPause: mockPause,
+ onResume: mockResume,
+ });
+
+ useEffect(() => {
+ start();
+
+ setTimeout(() => pause(), 550);
+ setTimeout(() => resume(), 1100);
+ }, []);
- expect(tree.toJSON().children.join('')).toEqual('0:0:0:3');
+ return {timeLeft}
;
+ }
+
+ const tree = renderer.create();
+
+ // For render
+ await asyncFn(150);
+ expect(tree.toJSON().children.join('')).toEqual('900');
+
+ await asyncFn(500);
+ expect(tree.toJSON().children.join('')).toEqual('500');
+
+ // still paused
+ await asyncFn(100);
+ expect(tree.toJSON().children.join('')).toEqual('500');
+
+ await asyncFn(500);
+ expect(tree.toJSON().children.join('')).toEqual('400');
+ await asyncFn(450);
+ expect(tree.toJSON().children.join('')).toEqual('0');
+
+ expect(mockPause).toHaveBeenCalledTimes(1);
+ expect(mockResume).toHaveBeenCalledTimes(1);
+ });
+
+ it('reset', async() => {
+ const mockReset = jest.fn();
+
+ function App() {
+ const [timeLeft, { start, reset }] = useCountDown(1000, 100, {
+ onReset: mockReset,
+ });
+
+ useEffect(() => {
+ start();
+ setTimeout(() => reset(), 550);
+ }, []);
+
+ return {timeLeft}
;
+ }
+
+ const tree = renderer.create();
+
+ // For render
+ await asyncFn(150);
+
+ expect(tree.toJSON().children.join('')).toEqual('900');
+
+ await asyncFn(500);
+ expect(tree.toJSON().children.join('')).toEqual('1000');
+
+ await asyncFn(100);
+ expect(tree.toJSON().children.join('')).toEqual('1000');
+
+ expect(mockReset).toHaveBeenCalledTimes(1);
+ });
+
+ it('events', async() => {
+ const mockStart = jest.fn();
+ const mockTick = jest.fn();
+ const mockCompleted = jest.fn();
+ function App() {
+ const [timeLeft, { start }] = useCountDown(1000, 100, {
+ onStart: mockStart,
+ onTick: mockTick,
+ onCompleted: mockCompleted,
+ });
+
+ useEffect(() => {
+ start();
+ }, []);
+
+ return {timeLeft}
;
+ }
+
+ const tree = renderer.create();
+
+ // For render
+ await asyncFn(1100);
- await asyncFn(3000);
+ expect(mockStart).toHaveBeenCalledTimes(1);
+ expect(mockCompleted).toHaveBeenCalledTimes(1);
- expect(tree.toJSON().children.join('')).toEqual('0:0:0:0');
+ expect(mockTick).toHaveBeenCalledTimes(10);
+ expect(mockTick).toHaveBeenNthCalledWith(1, 1000);
+ expect(mockTick).toHaveBeenNthCalledWith(2, 900);
+ expect(mockTick).toHaveBeenNthCalledWith(3, 800);
+ expect(mockTick).toHaveBeenNthCalledWith(4, 700);
+ expect(mockTick).toHaveBeenNthCalledWith(5, 600);
+ expect(mockTick).toHaveBeenNthCalledWith(6, 500);
+ expect(mockTick).toHaveBeenNthCalledWith(7, 400);
+ expect(mockTick).toHaveBeenNthCalledWith(8, 300);
+ expect(mockTick).toHaveBeenNthCalledWith(9, 200);
+ expect(mockTick).toHaveBeenNthCalledWith(10, 100);
});
});
diff --git a/packages/useCountDown/src/index.js b/packages/useCountDown/src/index.js
index f9d78b1..9d6bee0 100644
--- a/packages/useCountDown/src/index.js
+++ b/packages/useCountDown/src/index.js
@@ -1,91 +1,123 @@
-import { useRef, useState, useEffect } from 'rax';
+import { useState, useRef, useEffect, useMemo, useCallback } from 'rax';
-const DAY_SECOND = 24 * 3600;
-const HOUR_SECOND = 3600;
-const MINUTES_SECOND = 60;
+// use setTimeout/clearTimeout as fallback
+const rAF = typeof requestAnimationFrame !== 'undefined' ?
+ requestAnimationFrame :
+ setTimeout;
+const cAF = typeof cancelAnimationFrame !== 'undefined' ?
+ cancelAnimationFrame :
+ clearTimeout;
-export default function(start, end) {
- if (!isNumber(start) || !isNumber(end)) {
- throw new Error('Start or end time should be number.');
+const useCountDown = (timeToCount = 60 * 1000, interval = 1000, events = {}) => {
+ const {onStart, onTick, onPause, onResume, onCompleted, onReset} = events;
+ const [timeLeft, setTimeLeft] = useState(timeToCount);
+ const timer = useRef({});
+
+ useEffect(() => {
+ if (timeLeft > 0) {
+ if (typeof onTick === 'function') {
+ onTick(timeLeft);
+ }
+ } else if (timeLeft === 0) {
+ if (typeof onCompleted === 'function') {
+ onCompleted();
+ }
+ }
+ }, [timeLeft]);
+
+ const run = (ts) => {
+ const timestamp = ts || new Date().getTime();
+
+ if (!timer.current.started) {
+ timer.current.started = timestamp;
+ }
+
+ const elapsed = Math.max(timestamp - timer.current.started, 0);
+ const totalInterval = Math.round(elapsed / interval) * interval;
+ const time = timer.current.timeToCount - totalInterval;
+
+ if (time !== timer.current.timeLeft) {
+ timer.current.timeLeft = time;
+ setTimeLeft(time);
+ }
+
+ if (elapsed < timer.current.timeToCount) {
+ // not finished
+ timer.current.requestId = rAF(run, interval);
+ } else {
+ // finished
+ timer.current = {};
+ setTimeLeft(0);
+ }
};
- if (start < end) {
- throw new Error('Start time should be greater than end time.');
- }
+ const start = useCallback(
+ (ttc) => {
+ cAF(timer.current.requestId);
- const ref = useRef(null);
+ const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
+ timer.current.started = null;
+ timer.current.timeToCount = newTimeToCount;
+ timer.current.requestId = rAF(run, interval);
- if (!ref.remainTime) {
- ref.remainTime = formatTime(parseInt((start - end) / 1000));
- }
- const [timeLeft, setTimeLeft] = useState(ref.remainTime);
+ setTimeLeft(newTimeToCount);
- useEffect(() => {
- let shouldStop = false;
- timeCountDown(ref, () => {
- let remainTime = ref.remainTime;
- if (remainTime.seconds > 0) {
- remainTime.seconds--;
- } else {
- if (remainTime.minutes > 0) {
- remainTime.seconds = 59;
- remainTime.minutes--;
- } else {
- if (remainTime.hours > 0) {
- Object.assign(remainTime, {
- hours: remainTime.hours - 1,
- minutes: 59,
- seconds: 59,
- });
- } else {
- if (remainTime.days > 0) {
- Object.assign(remainTime, {
- days: remainTime.days - 1,
- hours: 23,
- minutes: 59,
- seconds: 59,
- });
- } else {
- Object.assign(remainTime, {
- days: 0,
- hours: 0,
- minutes: 0,
- seconds: 0,
- });
- shouldStop = true;
- }
- }
+ if (typeof onStart === 'function') {
+ onStart();
+ }
+ },
+ [interval]
+ );
+
+ const pause = useCallback(
+ () => {
+ cAF(timer.current.requestId);
+ timer.current.started = null;
+ timer.current.timeToCount = timer.current.timeLeft;
+ if (typeof onPause === 'function') {
+ onPause();
+ }
+ },
+ []
+ );
+
+ const resume = useCallback(
+ () => {
+ if (!timer.current.started && timer.current.timeLeft > 0) {
+ cAF(timer.current.requestId);
+ timer.current.requestId = rAF(run, interval);
+ if (typeof onResume === 'function') {
+ onResume();
}
}
- setTimeLeft({
- ...remainTime
- });
- return shouldStop;
- });
- return () => clearTimeout(ref.id);
- }, []);
+ },
+ [interval]
+ );
- return timeLeft;
-}
+ const reset = useCallback(
+ () => {
+ if (timer.current.timeLeft) {
+ cAF(timer.current.requestId);
+ timer.current = {};
+ setTimeLeft(timeToCount);
+ if (typeof onReset === 'function') {
+ onReset();
+ }
+ }
+ },
+ [timeToCount]
+ );
-function formatTime(difference) {
- return {
- days: parseInt(difference / DAY_SECOND),
- hours: parseInt(difference % DAY_SECOND / HOUR_SECOND),
- minutes: parseInt(difference % HOUR_SECOND / MINUTES_SECOND),
- seconds: difference % MINUTES_SECOND,
- };
-}
+ const actions = useMemo(
+ () => ({ start, pause, resume, reset }),
+ []
+ );
+
+ useEffect(() => {
+ return () => cAF(timer.current.requestId);
+ }, []);
-function isNumber(val) {
- return typeof val === 'number';
-}
+ return [timeLeft, actions];
+};
-function timeCountDown(ref, callback) {
- ref.id = setTimeout(() => {
- const shouldStop = callback();
- if (!shouldStop) {
- timeCountDown(ref, callback);
- }
- }, 1000);
-}
+export default useCountDown;
\ No newline at end of file
diff --git a/packages/useRouter/package.json b/packages/useRouter/package.json
index d337fa4..f34df70 100644
--- a/packages/useRouter/package.json
+++ b/packages/useRouter/package.json
@@ -23,7 +23,7 @@
"devDependencies": {
"driver-server": "^1.0.0",
"history": "^5.0.0",
- "rax": "^1.0.0"
+ "rax-test-renderer": "^1.0.0"
},
"dependencies": {
"path-to-regexp": "^6.0.0"