├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── SingleWaiter.ts ├── Waiter.ts ├── createFixedWait.ts ├── defaultWaiter.ts ├── index.ts ├── promiseFinally.ts ├── types.ts ├── useLocalWait.ts └── useWaitBuffer.ts ├── test ├── createFixedWait.ts ├── singleWaiter.ts ├── useLocalWait.ts └── waiter.ts ├── tsconfig.json └── website ├── app.tsx ├── dummy_texts.json ├── favicon.ico ├── index.html ├── style.scss └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .cache/ 4 | public/ 5 | *.lock 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2019 skt-t1-byungi 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-waiter 🤵 2 | > A react hook to wait for an asynchronous order. 3 | 4 | [](https://www.npmjs.com/package/use-waiter) 5 | [](https://github.com/skt-t1-byungi/use-waiter/blob/master/LICENSE) 6 | 7 | ##### Demo 8 | https://skt-t1-byungi.github.io/use-waiter/ 9 | 10 | ## Install 11 | ```sh 12 | npm i use-waiter 13 | ``` 14 | 15 | ## Example 16 | ```jsx 17 | import {useWait, wait} from 'use-waiter' 18 | 19 | function App(){ 20 | const isSending = useWait('SEND_MESSAGE', {delay: 300, duration: 600}) 21 | 22 | if(isSending){ 23 | return 24 | } 25 | 26 | const onClick = () => { 27 | wait('SEND_MESSAGE', sendMessageAsync('hello')) 28 | } 29 | 30 | return ( 31 | <> 32 | 33 | send! 34 | >) 35 | } 36 | ``` 37 | 38 | ## API 39 | ### wait(name, order) 40 | Wait for an asynchronous order. Orders should be promise or function. Returns the order promise. 41 | 42 | ```js 43 | // promise order 44 | const promise = sendMessageAsync('hello') 45 | wait('SEND_MESSAGE', promise) 46 | 47 | // function order 48 | wait('SEND_MESSAGE', async () => { 49 | await sendMessageAsync('hello') 50 | }) 51 | ``` 52 | 53 | ### isWaiting(name) 54 | Returns whether order is waiting or not. 55 | ```js 56 | import {isWaiting, wait} from 'use-waiter' 57 | 58 | isWaiting('TASK') // => false 59 | 60 | wait('TASK', asyncTask()).then(() => { 61 | isWaiting('TASK') // => false 62 | }) 63 | 64 | isWaiting('TASK') // => true 65 | ``` 66 | 67 | ### useWait(name[, opts]) 68 | A react hook that subscribes to changes in order status. 69 | 70 | #### options 71 | ##### `delay` 72 | When promise changes to pending, respond as late as the delay time. if promise is completed within the delay time, it does not respond. This prevents flashing when the pending time is short.The default value is `0`ms. 73 | 74 | ##### `duration` 75 | When the promise is completed before the duration time, respond after the remaining duration time. This will ensure minimum motion time to prevent flashing. The default value is `0`ms. 76 | 77 | ### new Waiter() 78 | Create an independent waiter instance. 79 | ```js 80 | import {Waiter, isWaiting} from 'use-waiter' 81 | 82 | const waiter = new Waiter() 83 | waiter.wait('TASK', asyncTask()) 84 | 85 | waiter.isWaiting('TASK') // => true 86 | isWaiting('TASK') // => false 87 | ``` 88 | 89 | ### useLocalWait([opts]) 90 | A react hook for local use within a component. 91 | 92 | ```jsx 93 | import {useLocalWait} from 'use-waiter' 94 | 95 | function App(){ 96 | const [isSending, wait] = useLocalWait({delay: 300, duration: 600}) 97 | 98 | if(isSending){ 99 | return 100 | } 101 | 102 | const onClick = () => { 103 | wait(sendMessageAsync('hello')) 104 | } 105 | 106 | return ( 107 | <> 108 | 109 | send! 110 | >) 111 | } 112 | ``` 113 | 114 | #### options 115 | Same as [`useWait` options](#options). 116 | 117 | ### createFixedWait(orderer) 118 | Create a waiter that performs a fixed single order. 119 | 120 | ```jsx 121 | import {createFixedWait} from 'use-waiter' 122 | 123 | const sendMessage = createFixedWait(async (text) => { 124 | await sendMessageAsync(text) 125 | }) 126 | 127 | function App(){ 128 | const isSending = sendMessage.useWait({delay: 300, duration: 600}) 129 | 130 | if(isSending){ 131 | return 132 | } 133 | 134 | const onClick = () => { 135 | sendMessage('hello') 136 | } 137 | 138 | return ( 139 | <> 140 | 141 | send! 142 | >) 143 | } 144 | ``` 145 | 146 | ### useWaitBuffer(isWaiting, value) 147 | If you use `duration`, you are still `waiting`, even though the actual asynchronous request is finished. 148 | This is useful if you want to show the results of the request after waiting. 149 | 150 | ```js 151 | import {useWait, useWaitBuffer} from 'use-waiter' 152 | 153 | function App(){ 154 | const {data} = useContext(ApiContext) 155 | const isWaiting = useWait('API_FETCHED_DATA', {duration: 600}) 156 | const displayData = useWaitBuffer(isWaiting, data) 157 | 158 | /* ... */ 159 | } 160 | ``` 161 | 162 | ## License 163 | MIT 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-waiter", 3 | "description": "A react hook to wait for an asynchronous order.", 4 | "version": "2.2.1", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "https://github.com/skt-t1-byungi/use-waiter", 8 | "homepage": "https://skt-t1-byungi.github.io/use-waiter/", 9 | "author": "skt-t1-byungi ", 10 | "keywords": [ 11 | "react", 12 | "hooks", 13 | "wait", 14 | "waiting", 15 | "queue", 16 | "load", 17 | "loadable", 18 | "loading" 19 | ], 20 | "license": "MIT", 21 | "scripts": { 22 | "dev": "parcel website/index.html -d public", 23 | "website": "parcel build website/index.html -d public --public-url ./ --no-source-maps", 24 | "deploy": "rm -rf public && npm run website && gh-pages -d public", 25 | "test": "ava", 26 | "build": "rm -rf dist && tsc", 27 | "prepublishOnly": "npm run test && npm run build" 28 | }, 29 | "engines": { 30 | "node": ">= 6" 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "peerDependencies": { 36 | "react": "^16.8.6" 37 | }, 38 | "dependencies": { 39 | "rsup-duration": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "@byungi/p-delay": "^0.1.3", 43 | "@testing-library/react-hooks": "^3.1.1", 44 | "@types/react-dom": "^16.8.5", 45 | "ava": "^2.2.0", 46 | "clsx": "^1.0.4", 47 | "eslint": "^6.6.0", 48 | "eslint-config-byungi": "^0.7.6", 49 | "gh-pages": "2.1.1", 50 | "parcel-bundler": "^1.12.4", 51 | "react": "^16.11.0", 52 | "react-dom": "^16.11.0", 53 | "react-test-renderer": "^16.11.0", 54 | "sass": "^1.22.9", 55 | "thejungle": "^1.0.0", 56 | "ts-node": "^8.4.1", 57 | "typescript": "^3.5.3", 58 | "use-simple-store": "^1.2.8" 59 | }, 60 | "ava": { 61 | "compileEnhancements": false, 62 | "extensions": [ 63 | "ts" 64 | ], 65 | "require": [ 66 | "ts-node/register" 67 | ] 68 | }, 69 | "eslintConfig": { 70 | "extends": "byungi/typescriptreact" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/SingleWaiter.ts: -------------------------------------------------------------------------------- 1 | import { Order, WaitOpts } from './types' 2 | import { useState, useMemo, useLayoutEffect } from 'react' 3 | import createDuration from 'rsup-duration' 4 | import promiseFinally from './promiseFinally' 5 | 6 | export default class SingleWaiter { 7 | private _count = 0 8 | private _listeners: Array<() => void> = [] 9 | 10 | constructor () { 11 | this.wait = this.wait.bind(this) 12 | this.useWait = this.useWait.bind(this) 13 | } 14 | 15 | get isInUse () { 16 | return this.isWaiting || this._listeners.length > 0 17 | } 18 | 19 | get isWaiting () { 20 | return this._count > 0 21 | } 22 | 23 | private _emit () { 24 | this._listeners.forEach(fn => fn()) 25 | } 26 | 27 | private _subscribe (listener: () => void) { 28 | this._listeners.push(listener) 29 | return () => { 30 | this._listeners.splice(this._listeners.indexOf(listener), 1) 31 | } 32 | } 33 | 34 | wait (order: Order) { 35 | if (++this._count === 1) this._emit() 36 | 37 | return promiseFinally(order, () => { 38 | if (--this._count === 0) this._emit() 39 | }) 40 | } 41 | 42 | // tslint:disable-next-line: cognitive-complexity 43 | useWait ({ delay = 0, duration = 0 }: WaitOpts = {}) { 44 | const [isWaiting, setWaiting] = useState(this.isWaiting) 45 | 46 | const memoUnSubscriber = useMemo(() => { 47 | const delayer = delay > 0 ? createDuration(delay) : null 48 | const persister = duration > 0 ? createDuration(duration) : null 49 | 50 | let next: boolean | null = null 51 | let unmounted = false 52 | 53 | if (this.isWaiting && persister) { 54 | persister.start().then(afterDuration) 55 | } 56 | 57 | const unsubscribe = this._subscribe(() => { 58 | if ((delayer && delayer.isDuring) || (persister && persister.isDuring)) { 59 | next = this.isWaiting 60 | return 61 | } 62 | 63 | if (this.isWaiting) { 64 | if (delayer) { 65 | next = true 66 | delayer.start().then(afterDelay) 67 | } else { 68 | startWaiting() 69 | } 70 | } else { 71 | setWaiting(false) 72 | } 73 | }) 74 | 75 | return () => { 76 | unmounted = true 77 | unsubscribe() 78 | } 79 | 80 | function startWaiting () { 81 | setWaiting(true) 82 | if (persister) persister.start().then(afterDuration) 83 | } 84 | 85 | function afterDelay () { 86 | if (unmounted) return 87 | if (next === true) startWaiting() 88 | next = null 89 | } 90 | 91 | function afterDuration () { 92 | if (unmounted) return 93 | if (next === false) setWaiting(false) 94 | next = null 95 | } 96 | }, [delay, duration]) 97 | 98 | useLayoutEffect(() => memoUnSubscriber, [memoUnSubscriber]) 99 | 100 | return isWaiting 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Waiter.ts: -------------------------------------------------------------------------------- 1 | import SingleWaiter from './SingleWaiter' 2 | import { useLayoutEffect } from 'react' 3 | import { WaitOpts, Order } from './types' 4 | import promiseFinally from './promiseFinally' 5 | 6 | export default class Waiter { 7 | private _waiters: Record = Object.create(null) 8 | 9 | constructor () { 10 | this.wait = this.wait.bind(this) 11 | this.useWait = this.useWait.bind(this) 12 | } 13 | 14 | isWaiting (name: string | number) { 15 | return Boolean(this._waiters[name] && this._waiters[name].isWaiting) 16 | } 17 | 18 | private _getWaiter (name: string | number) { 19 | return this._waiters[name] || (this._waiters[name] = new SingleWaiter()) 20 | } 21 | 22 | wait (name: string | number, order: Order) { 23 | const waiter = this._getWaiter(name) 24 | 25 | return promiseFinally(waiter.wait(order), () => { 26 | if (!waiter.isInUse) delete this._waiters[name] 27 | }) 28 | } 29 | 30 | useWait (name: string | number, opts: WaitOpts = {}) { 31 | const waiter = this._getWaiter(name) 32 | const isWaiting = waiter.useWait(opts) 33 | 34 | useLayoutEffect(() => () => { 35 | if (!waiter.isInUse) delete this._waiters[name] 36 | }, [name, waiter.isInUse]) 37 | 38 | return isWaiting 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/createFixedWait.ts: -------------------------------------------------------------------------------- 1 | import SingleWaiter from './SingleWaiter' 2 | import { AnyFn } from './types' 3 | 4 | export default function createFixedWait (orderer: Orderer) { 5 | const waiter = new SingleWaiter() 6 | 7 | const order = (...args: Parameters) => waiter.wait>(orderer(...args)) 8 | order.useWait = waiter.useWait 9 | 10 | return order 11 | } 12 | -------------------------------------------------------------------------------- /src/defaultWaiter.ts: -------------------------------------------------------------------------------- 1 | import Waiter from './Waiter' 2 | import { WaitOpts, Order } from './types' 3 | 4 | const waiter = new Waiter() 5 | 6 | export function isWaiting (name: string | number) { 7 | return waiter.isWaiting(name) 8 | } 9 | 10 | export function useWait (name: string | number, opts: WaitOpts = {}) { 11 | return waiter.useWait(name, opts) 12 | } 13 | 14 | export function wait (name: string | number, order: Order) { 15 | return waiter.wait(name, order) 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useWaitBuffer } from './useWaitBuffer' 2 | export { default as useLocalWait } from './useLocalWait' 3 | export { default as createFixedWait } from './createFixedWait' 4 | export { default as Waiter } from './Waiter' 5 | export * from './defaultWaiter' 6 | -------------------------------------------------------------------------------- /src/promiseFinally.ts: -------------------------------------------------------------------------------- 1 | import { Order } from './types' 2 | 3 | export default function promiseFinally (order: Order, onFinally: () => void) { 4 | const promise = Promise.resolve(typeof order === 'function' ? order() : order) 5 | promise.then(onFinally, onFinally) 6 | return promise 7 | } 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type AnyFn = (...args: any) => any 2 | export type Order = (() => T) | Promise 3 | export type WaitFn = (order: Order) => Promise 4 | export interface WaitOpts { delay?: number; duration?: number } 5 | -------------------------------------------------------------------------------- /src/useLocalWait.ts: -------------------------------------------------------------------------------- 1 | import SingleWaiter from './SingleWaiter' 2 | import { useMemo } from 'react' 3 | import { WaitFn, WaitOpts } from './types' 4 | 5 | export default function useLocalWait (opts: WaitOpts = {}): [boolean, WaitFn] { 6 | const waiter = useMemo(() => new SingleWaiter(), []) 7 | 8 | const isWaiting = waiter.useWait(opts) 9 | 10 | return [isWaiting, waiter.wait] 11 | } 12 | -------------------------------------------------------------------------------- /src/useWaitBuffer.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | export default function useWaitBuffer (isWaiting: boolean, val: T) { 4 | const bufRef = useRef(val) 5 | 6 | useEffect(() => { 7 | if (!isWaiting) bufRef.current = val 8 | }, [isWaiting, val]) 9 | 10 | return isWaiting ? bufRef.current : val 11 | } 12 | -------------------------------------------------------------------------------- /test/createFixedWait.ts: -------------------------------------------------------------------------------- 1 | import { serial as test } from 'ava' 2 | import { renderHook } from '@testing-library/react-hooks' 3 | import { createFixedWait } from '../src' 4 | import delay from '@byungi/p-delay' 5 | 6 | test('share a order state', async t => { 7 | const order = createFixedWait(delay) 8 | const { result: r1 } = renderHook(() => order.useWait()) 9 | const { result: r2 } = renderHook(() => order.useWait()) 10 | 11 | t.false(r1.current) 12 | t.false(r2.current) 13 | order(50) 14 | t.true(r1.current) 15 | t.true(r2.current) 16 | await delay(50) 17 | t.false(r1.current) 18 | t.false(r2.current) 19 | }) 20 | 21 | test('different options', async t => { 22 | const order = createFixedWait(delay) 23 | const { result: r1 } = renderHook(() => order.useWait({ duration: 80 })) 24 | const { result: r2 } = renderHook(() => order.useWait({ delay: 30 })) 25 | 26 | order(50) 27 | t.true(r1.current) 28 | t.false(r2.current) 29 | await delay(30) 30 | t.true(r1.current) 31 | t.true(r2.current) 32 | await delay(20) 33 | t.true(r1.current) 34 | t.false(r2.current) 35 | await delay(30) 36 | t.false(r1.current) 37 | t.false(r2.current) 38 | }) 39 | -------------------------------------------------------------------------------- /test/singleWaiter.ts: -------------------------------------------------------------------------------- 1 | import { serial as test } from 'ava' 2 | import SingleWaiter from '../src/SingleWaiter' 3 | import delay from '@byungi/p-delay' 4 | 5 | test('isWaiting', async t => { 6 | const w = new SingleWaiter() 7 | 8 | // one 9 | t.false(w.isWaiting) 10 | const p1 = w.wait(delay(0)) 11 | t.true(w.isWaiting) 12 | await p1 13 | t.false(w.isWaiting) 14 | 15 | // overlapped 16 | const p2 = w.wait(delay(50)) 17 | await delay(30) 18 | t.true(w.isWaiting) 19 | const p3 = w.wait(delay(50)) 20 | await p2 21 | t.true(w.isWaiting) 22 | await p3 23 | t.false(w.isWaiting) 24 | }) 25 | 26 | test('function type order', async t => { 27 | const w = new SingleWaiter() 28 | const p = w.wait(() => delay(50)) 29 | t.true(w.isWaiting) 30 | await p 31 | t.false(w.isWaiting) 32 | }) 33 | 34 | test('handled errors should be silent.', async t => { 35 | const w = new SingleWaiter() 36 | // eslint-disable-next-line prefer-promise-reject-errors 37 | await t.notThrowsAsync(w.wait(Promise.reject('error')).catch(() => {})) 38 | }) 39 | -------------------------------------------------------------------------------- /test/useLocalWait.ts: -------------------------------------------------------------------------------- 1 | import { serial as test } from 'ava' 2 | import { renderHook } from '@testing-library/react-hooks' 3 | import { useLocalWait } from '../src' 4 | import delay from '@byungi/p-delay' 5 | 6 | test('change isWaiting', async t => { 7 | const { result } = renderHook(() => useLocalWait()) 8 | const [isWaiting, wait] = result.current 9 | 10 | t.false(isWaiting) 11 | const p = wait(delay(0)) 12 | t.true(result.current[0]) 13 | await p 14 | t.false(result.current[0]) 15 | }) 16 | 17 | test('on overlapped', async t => { 18 | let calls = 0 19 | const { result: { current: [, wait] } } = renderHook(() => (calls++, useLocalWait())) 20 | 21 | for (let i = 0; i < 4; i++) { 22 | wait(delay(100)) 23 | await delay(50) 24 | } 25 | t.is(calls, 2) 26 | 27 | await delay(50) 28 | t.is(calls, 3) 29 | }) 30 | 31 | test('delay option', async t => { 32 | const { result } = renderHook(() => useLocalWait({ delay: 100 })) 33 | const [isWaiting, wait] = result.current 34 | 35 | // short case 36 | t.false(isWaiting) 37 | wait(delay(50)) 38 | t.false(result.current[0]) 39 | await delay(50) 40 | t.false(result.current[0]) 41 | await delay(50) 42 | t.false(result.current[0]) 43 | 44 | // long case 45 | wait(delay(150)) 46 | t.false(result.current[0]) 47 | await delay(50) 48 | t.false(result.current[0]) 49 | await delay(50) 50 | t.true(result.current[0]) 51 | }) 52 | 53 | test('duration option', async t => { 54 | const { result } = renderHook(() => useLocalWait({ duration: 100 })) 55 | const [, wait] = result.current 56 | 57 | wait(delay(50)) 58 | t.true(result.current[0]) 59 | await delay(50) 60 | t.true(result.current[0]) 61 | await delay(40) 62 | t.true(result.current[0]) 63 | await delay(10) 64 | t.false(result.current[0]) 65 | }) 66 | 67 | test('complex option', async t => { 68 | const { result } = renderHook(() => useLocalWait({ delay: 50, duration: 100 })) 69 | const [, wait] = result.current 70 | 71 | wait(delay(80)) 72 | await delay(50) 73 | t.true(result.current[0]) 74 | await delay(50) 75 | t.true(result.current[0]) 76 | await delay(40) 77 | t.true(result.current[0]) 78 | await delay(10) 79 | t.false(result.current[0]) 80 | }) 81 | -------------------------------------------------------------------------------- /test/waiter.ts: -------------------------------------------------------------------------------- 1 | import { serial as test } from 'ava' 2 | import { Waiter } from '../src' 3 | import { renderHook } from '@testing-library/react-hooks' 4 | import delay from '@byungi/p-delay' 5 | 6 | test('isWaiting', async t => { 7 | const w = new Waiter() 8 | 9 | t.false(w.isWaiting('a')) 10 | t.false(w.isWaiting('b')) 11 | const p1 = w.wait('a', delay(50)) 12 | const p2 = w.wait('b', delay(80)) 13 | t.true(w.isWaiting('a')) 14 | t.true(w.isWaiting('b')) 15 | await p1 16 | t.false(w.isWaiting('a')) 17 | t.true(w.isWaiting('b')) 18 | await p2 19 | t.false(w.isWaiting('b')) 20 | }) 21 | 22 | test('clear not used', async t => { 23 | const w = new Waiter() 24 | const p = w.wait('a', delay(0)) 25 | t.truthy((w as any)._waiters.a) 26 | await p 27 | t.falsy((w as any)._waiters.a) 28 | 29 | const { unmount: um1 } = renderHook(() => w.useWait('a')) 30 | const { unmount: um2 } = renderHook(() => w.useWait('a')) 31 | t.truthy((w as any)._waiters.a) 32 | um1() 33 | t.truthy((w as any)._waiters.a) 34 | um2() 35 | t.falsy((w as any)._waiters.a) 36 | }) 37 | 38 | test('At the time of initial rendering, duration should work immediately if waiting.', async t => { 39 | const w = new Waiter() 40 | w.wait('a', delay(50)) 41 | const { result } = renderHook(() => w.useWait('a', { delay: 30, duration: 100 })) 42 | t.true(result.current) 43 | await delay(50) 44 | t.true(result.current) 45 | }) 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["esnext", "dom"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | }, 10 | "include": [ 11 | "src/" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /website/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import createStore from 'use-simple-store' 4 | import DUMMY_TEXTS from './dummy_texts.json' 5 | import { createFixedWait, useWaitBuffer } from '../src' 6 | import delay from '@byungi/p-delay' 7 | import cx from 'clsx' 8 | 9 | const waitDelay = createFixedWait(delay) 10 | 11 | const WAITER_OPTS = [ 12 | { delay: 0, duration: 0 }, 13 | { delay: 300, duration: 0 }, 14 | { delay: 300, duration: 900 } 15 | ] 16 | 17 | const store = createStore({ textIdx: 0, selectedOptIdx: 0 }) 18 | 19 | const App = () => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 🤵 27 | use-waiter 28 | A react hook to wait for an asynchronous order. 29 | 30 | 31 | 32 | {[50, 400, 800].map(t => { 33 | const onBtnClick = async () => { 34 | await waitDelay(t) 35 | store.update(s => { 36 | s.textIdx = (s.textIdx + 1) % DUMMY_TEXTS.length 37 | }) 38 | } 39 | return delay({t}) 40 | })} 41 | 42 | 45 | 46 | 47 | ) 48 | } 49 | 50 | const Example = ({ className }: {className?: string}) => { 51 | const { textIdx, selectedOptIdx } = store.useStore() 52 | const isNoneOpt = selectedOptIdx === -1 53 | const isWaiting = waitDelay.useWait(WAITER_OPTS[selectedOptIdx]) && !isNoneOpt 54 | const showIdx = useWaitBuffer(isWaiting, textIdx) 55 | 56 | return ( 57 | 58 | 59 | {DUMMY_TEXTS[showIdx]} 60 | {isWaiting && } 61 | 62 | ) 63 | } 64 | 65 | const Spinner = ({ className }: {className?: string}) => { 66 | return ( 67 | 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | const Options = ({ className }: {className?: string}) => { 76 | const selectedIdx = store.useStore(s => s.selectedOptIdx) 77 | return ( 78 | 79 | 80 | store.update(s => { s.selectedOptIdx = -1 })} 85 | checked={selectedIdx === -1} 86 | /> 87 | NO LOADER 88 | 89 | {WAITER_OPTS.map((opt, idx) => ( 90 | 91 | store.update(s => { s.selectedOptIdx = idx })} 96 | checked={idx === selectedIdx} 97 | /> 98 | {`useWait('DELAY', {delay: ${opt.delay}, duration: ${opt.duration}})`} 99 | ) 100 | )} 101 | ) 102 | } 103 | 104 | ReactDOM.render(, document.getElementById('app')) 105 | -------------------------------------------------------------------------------- /website/dummy_texts.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus, odio eu pretium accumsan, sem ante facilisis nibh, nec eleifend ante dolor a nunc. In a massa eros. Etiam dolor nulla, volutpat ut est et, sodales vulputate ligula. Vestibulum laoreet erat ipsum, quis molestie turpis sodales id. Aliquam pulvinar eget nibh varius vestibulum. Morbi quis eros et enim tincidunt sodales. Suspendisse a velit velit. Vestibulum sed lectus at turpis suscipit malesuada. Phasellus sem est, dignissim ac quam vitae, facilisis semper leo. Etiam turpis arcu, commodo non pretium at, lacinia quis mi. Pellentesque felis mauris, porta ac posuere at, iaculis a urna. In hac habitasse platea dictumst. Nam fringilla felis tellus, in dignissim orci porttitor id.", 3 | "Fusce interdum vulputate justo, in eleifend erat finibus sit amet. Curabitur et ligula non erat hendrerit ultricies. Etiam semper arcu nec urna tincidunt, eget feugiat mauris porttitor. Nam ac venenatis ante. Aliquam erat volutpat. Mauris auctor mollis imperdiet. Maecenas blandit leo sit amet nibh dictum, sed consequat nibh commodo. Suspendisse eu turpis a diam bibendum tempor. Nunc rutrum placerat porta. Ut posuere felis quis nisi efficitur aliquet. Nam venenatis nisi et urna suscipit ultrices. Aliquam ornare ex non libero tincidunt tristique. Integer posuere cursus enim, a varius sem interdum quis. Proin eu egestas tortor, et dictum massa. Suspendisse eget pulvinar ipsum. Suspendisse at libero vulputate mi viverra pretium.", 4 | "Aliquam dictum iaculis velit, eu laoreet urna mattis a. Proin sed augue purus. Donec vel felis orci. Fusce elementum auctor nunc, vitae dictum ipsum elementum id. Nulla orci magna, volutpat in lectus quis, euismod fringilla ipsum. Nam vitae purus velit. Quisque vehicula a lorem ac elementum. Suspendisse vitae dictum eros, in tempor enim. Mauris neque nunc, cursus ac malesuada nec, fringilla at dui. Curabitur quis molestie nisl, non ultrices risus. In diam ante, aliquet non felis ac, luctus malesuada neque.", 5 | "Ut pellentesque odio quis odio tempus egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec ultricies libero eget erat tincidunt eleifend. In lacinia mauris metus, eu rutrum est vulputate nec. Etiam facilisis pellentesque dignissim. Nam gravida fermentum ante in tristique. Donec ac cursus odio. Donec a convallis diam. Sed gravida interdum tellus, in ultrices eros.", 6 | "Morbi commodo libero at laoreet hendrerit. Nunc sodales fringilla nulla sed sagittis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam imperdiet facilisis posuere. Donec vel odio et metus venenatis ultrices. Morbi faucibus tincidunt felis eget elementum. Aliquam nec gravida mi. Proin venenatis auctor hendrerit. Donec vitae quam quis nibh vehicula vulputate euismod vitae nunc. Vestibulum non nulla ac turpis pellentesque viverra a id ipsum." 7 | ] 8 | -------------------------------------------------------------------------------- /website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skt-t1-byungi/use-waiter/f645192c7034cde8a3476b60e895768d1d87ef59/website/favicon.ico -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | use-waiter :: A react hook to wait for an asynchronous order. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/style.scss: -------------------------------------------------------------------------------- 1 | @import '~thejungle/reset'; 2 | @import '~thejungle/media'; 3 | @import '~thejungle'; 4 | 5 | $g-spinner-size: 80px; 6 | 7 | html, body{ 8 | font-family: 'Overpass Mono', monospace; 9 | font-size: 14px; 10 | color: #222; 11 | 12 | @include media('<=768px'){ font-size: 12px; } 13 | @include media('<=480px'){ font-size: 10px; } 14 | } 15 | 16 | .app{ 17 | font-size: 1rem; 18 | display: flex; 19 | min-height: 100vh; 20 | align-items: stretch; 21 | 22 | @include media('<=1200px'){ 23 | flex-direction: column-reverse; 24 | justify-content: flex-end 25 | } 26 | 27 | &__sec{ flex: 0 0 50%; } 28 | } 29 | 30 | @mixin app-section { 31 | display: flex; 32 | align-items: center; 33 | padding: 0 em(80px); 34 | 35 | @include media('<=1200px'){ justify-content: center } 36 | 37 | @include media('<=1200px'){ 38 | padding: em(40px) em(80px); 39 | justify-content: center; 40 | } 41 | 42 | @include media('<=768px'){ padding: em(20px) em(40px); } 43 | } 44 | 45 | @mixin app-section-inner { 46 | width: 100%; 47 | @include media('<=1200px'){ max-width: 80%; } 48 | @include media('<=768px'){ max-width: unset; } 49 | } 50 | 51 | .exam{ 52 | @include app-section; 53 | font-size: 1rem; 54 | background: #17223b; 55 | color: #f1f1e6; 56 | justify-content: flex-end; 57 | flex-grow: 1; 58 | 59 | &__inner{ 60 | @include app-section-inner; 61 | position: relative; 62 | font-size: em(20px); 63 | max-width: em(560px, 20px); 64 | line-height: 1.6; 65 | transition: color ease 0.2s; 66 | 67 | &--loading{ color: change-color(#f1f1e6, $alpha: 0.2); } 68 | } 69 | 70 | &__spinner{ 71 | top: calc(50% - #{$g-spinner-size / 2}); 72 | left: calc(50% - #{$g-spinner-size / 2}); 73 | } 74 | } 75 | 76 | .main{ 77 | @include app-section; 78 | font-size: 1rem; 79 | 80 | &__inner{ 81 | @include app-section-inner; 82 | line-height: 1.6; 83 | max-width: em(580px); 84 | } 85 | 86 | &__head{ margin-bottom: em(60px); } 87 | &__opts{ margin-bottom: em(20px); } 88 | &__btns{ margin-bottom: em(60px); } 89 | } 90 | 91 | .head{ 92 | font-size: 1rem; 93 | text-align: center; 94 | position: relative; 95 | 96 | &::after{ 97 | @include size(50%, em(50px)); 98 | content: ''; 99 | display: block; 100 | position: absolute; 101 | z-index: -1; 102 | bottom: em(5px); left: 30%; 103 | background: #FFF7D6; 104 | } 105 | 106 | &__logo{ 107 | line-height: 1; 108 | font-size: em(92px); 109 | margin-bottom: em(20px, 92px); 110 | padding-top: em(20px, 92px); 111 | } 112 | 113 | &__title{ 114 | font-family: 'Fredoka One', cursive; 115 | line-height: 1; 116 | font-size: em(42px); 117 | margin-bottom: em(15px, 42px); 118 | } 119 | 120 | &__desc{ 121 | font-size: em(18px); 122 | color: #888; 123 | } 124 | } 125 | 126 | .opts{ 127 | font-size: 1em; 128 | 129 | &__opt{ 130 | display: block; 131 | background: #f6f9ff; 132 | border: 1px solid #e5edffa6; 133 | padding: em(12px) em(28px); 134 | border-radius: 10px; 135 | 136 | &:not(last-child){ margin-bottom: em(10px); } 137 | } 138 | 139 | &__radio{ 140 | margin-right: em(18px); 141 | vertical-align: middle 142 | } 143 | } 144 | 145 | .btns{ 146 | font-size: 1rem; 147 | 148 | &__btn{ 149 | font-size: em(16px); 150 | @include column(3, em(12px)); 151 | font-family: 'Overpass Mono', monospace; 152 | padding: em(18px) 0; 153 | color: #222; 154 | } 155 | } 156 | 157 | .foot{ 158 | font-size: 1rem; 159 | text-align: center; 160 | padding: em(40px) 0; 161 | 162 | @include media('<=1200px'){ 163 | padding: 0; 164 | position: fixed; 165 | top: em(5px); right: em(10px); 166 | } 167 | 168 | &__link{ 169 | $color: #746549; 170 | $color-active: lighten($color, $amount: 18%); 171 | 172 | color: $color; 173 | text-decoration: none; 174 | border-bottom: 1px dashed $color-active; 175 | 176 | &:hover{ color: $color-active } 177 | } 178 | } 179 | 180 | .spinner{ 181 | $size: $g-spinner-size; 182 | $moved: $size*5/7; 183 | 184 | font-size: 1rem; 185 | position: absolute; 186 | z-index: 999; 187 | @include size($size); 188 | @include animate(1s infinite){ 189 | from { transform: rotate(0deg) } 190 | to { transform: rotate(360deg) } 191 | } 192 | 193 | &__i{ 194 | position: absolute; 195 | @include size($size*2/5); 196 | border-radius: $size/15; 197 | 198 | &--1{ 199 | left: 0; 200 | background-color: #5c6bc0; 201 | @include animate(2s linear 0s infinite normal){ 202 | 0% { transform: translate(0, 0); } 203 | 25% { transform: translate(0, $moved); } 204 | 50% { transform: translate($moved, $moved); } 205 | 75% { transform: translate($moved, 0); } 206 | } 207 | } 208 | 209 | &--2{ 210 | right: 0; 211 | background-color: #8bc34a; 212 | 213 | @include animate(2s linear 0s infinite normal){ 214 | 0% {transform: translate(0, 0); } 215 | 25% {transform: translate(-$moved, 0); } 216 | 50% {transform: translate(-$moved, $moved); } 217 | 75% {transform: translate(0, $moved); } 218 | } 219 | } 220 | 221 | &--3{ 222 | bottom: 0; 223 | background-color: #ffb74d; 224 | 225 | @include animate(2s linear 0s infinite normal){ 226 | 0% {transform: translate(0, 0); } 227 | 25% {transform: translate($moved, 0); } 228 | 50% {transform: translate($moved, -$moved); } 229 | 75% {transform: translate(0, -$moved); } 230 | } 231 | } 232 | 233 | &--4{ 234 | bottom: 0; 235 | right: 0; 236 | background-color: #f44336; 237 | 238 | @include animate(2s linear 0s infinite normal){ 239 | 0% {transform: translate(0, 0); } 240 | 25% {transform: translate(0, -$moved); } 241 | 50% {transform: translate(-$moved, -$moved); } 242 | 75% {transform: translate(-$moved, 0); } 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "jsx": "react", 6 | "esModuleInterop": true 7 | }, 8 | "include": [ 9 | "./" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------
A react hook to wait for an asynchronous order.