(React Hooks) カウントアップ・カウントダウンを実装してみた

# はじめに
React Hooksでタイマー機能を実装しました。(useCounter.tsx　※コードは[こちら](#usecountertsx))
demoのgifのようにカウントアップとカウントダウンの両パターンに対応してます。

コード全体はこちら↓（demoに示したもの）

https://github.com/fktlnz/react-hook-counter

# demo
カウントアップとカウントダウンの両方に対応
![movie2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187747/17cefaaa-9a3f-f1ee-bbbf-93ccbc174b96.gif)


# 使い方

| step | must/want| 説明 |
|:-|:-:|:-|
|(★1)useCounter.tsxを読み込み|  must | カウントを表示したいtsxファイルに読み込む  |
|(★2)必要な変数、関数を取り込む | must  | 必要な変数、関数を取り込む **※補足参照**  |
|(★3)カウントを表示| must  | カウントを表示したい箇所に**count**を記載  |
|(★4)カウント終了を検知| want  | カウント終了を検知してなにかする  |

※(★1)～(★4)は[コード](#呼び出し元のコード例demoで示したもの)内の記載と対応

## [補足] (★2)必要な変数、関数を取り込む
- 読み込み例
最低限、countとstartCountがあればOK
```tsx:
const {count, isTimerEnd, initCount, startCount} = useCounter();
```


- 取り込める変数

| 名前| Type|概要|
|:-:|:-:|:-|
| *count*  | number | カウント情報|
| *isTimerEnd*  | boolean | ***true***： カウントが終了した<br>***false***：カウント中 or リセット状態|

- 取り込める関数

| 名前|引数・返り値|概要|
|:-:|:-|:-|
| *initCount*  | f(引数なし)：返り値なし| カウントをリセット（*count*を0にして、isTimerEndをfalseとする）|
| *startCount*  | f( **upLimit?**：number, **downLimit?**：number, **direction?**：string)：返り値なし<br>**upLimit**：カウント上限<br>**downLimit**：カウント下限<br>**direction**：どちらにカウントするか（"up"の場合、下限⇒上限に向かってカウント、"down"の場合、上限⇒下限に向かってカウント）| カウントを開始する|

**(startCountの呼び出し例)**

```tsx
//引数なし：10⇒0のカウントダウンになる（デフォルト設定）
//デフォルト設定は、useCounter.tsx(4,5,6行目で変更可)
startCount();

//100⇒0のカウントダウン
startCount(100, 0, "down");

//0⇒100のカウントアップ
startCount(100, 0, "up");
```


## 呼び出し元のコード例(demoで示したもの)
```tsx:App.tsx
import React from 'react';
import './App.css';
import { useEffect, useState } from 'react';
import useCounter from './useCounter' //(★1)useCounter.tsxを読み込み

const EndDom:React.VFC<{isShow:boolean}>= (props) => {
  if(props.isShow) { return <p className="color-red">カウント終了！</p> }
  return <></>
}

function App() {
  const {count, isTimerEnd, initCount, startCount} = useCounter(); //(★2)必要な変数、関数を取り込む
  const [startTime, setStartTime] = useState<number>(0);
  const [endTime, setEndTime] = useState<number>(0);

  const start = (startTime:number, endTime:number) => {
    const direction = (startTime>endTime) ? "down" : "up";
    const upLimit = (direction==="down") ? startTime : endTime;
    const downLimit = (direction==="down") ? endTime : startTime;

    startCount(upLimit, downLimit, direction)
  }

  const reset = () => {
    initCount();
  }

  const handleChangeStartTime = (e:React.ChangeEvent<HTMLInputElement>) => {
    setStartTime(parseInt(e.target.value));
  }

  const handleChangeEndTime = (e:React.ChangeEvent<HTMLInputElement>) => {
    setEndTime(parseInt(e.target.value));
  }

  return (
    <div className="App">
      <main>
        <section className='counter-input mb10'>
          <input onChange={(e)=>handleChangeStartTime(e)} type="text" placeholder='開始時間(sec)'/>
          <input onChange={(e)=>handleChangeEndTime(e)} type="text" placeholder='終了時間(sec)'/>
        </section>
        <section className='counter-button'>
          <button onClick={()=>start(startTime, endTime)} className='button mb10'>START</button>
          <button onClick={()=>reset()} className='button mb10' disabled={(isTimerEnd===true) ? false : true}>RESET</button>
        </section>
        <section className={(isTimerEnd===true) ? 'counter-value color-red' : 'counter-value'}>
          {count} //(★3)カウントを表示
        </section>
        <EndDom isShow={isTimerEnd===true}/> //(★4)カウントが終了したら何かする
      </main>
    </div>
  );
}

export default App;


```


# useCounter.tsx
```tsx:useCounter.tsx
import { useEffect, useState } from "react";

//10秒⇒0秒のカウントダウンがデフォルト
//デフォルト変更可
const UPLIMIT = 10;
const DOWNLIMIT = 0;
const DIRECTION = "down";

const useCounter = () => {
    const [timeEnd, setTimeEnd] = useState(0);//終了時間
    const [direction, setDirection] = useState("down");//カウント方向
    const [count, setCount] = useState(0);//カウント情報
    const [timerFlag, setTimerFlag] = useState(false);//カウント開始フラグ
    const [isTimerEnd, setIsTimerEnd] = useState(false);//カウント終了フラグ

    useEffect(() => {
      if (timerFlag) {
        const id = setTimeout(() => {
          setCount((beforecount) => {
            console.log('timer・・・')
            if(direction==="up"){
              if(beforecount===timeEnd-1) {
                setTimerFlag(false);
                setIsTimerEnd(true);
                return (beforecount + 1);
              }
              if(beforecount<timeEnd){
                return (beforecount + 1);
              }
              return beforecount;
            }else{
              if(beforecount===timeEnd+1) {
                setTimerFlag(false);
                setIsTimerEnd(true);
                return (beforecount - 1);
              }
              if(beforecount>timeEnd){
                return (beforecount - 1);
              }
              return beforecount;
            }
          });
        }, 1000);
        return () => clearTimeout(id);
      }
      return;
    },[count, timerFlag]);

    //開始時間セット
    const setStart = (upLimit:number, downLimit:number, direction:string) => {
      const start = (direction==="up") ? downLimit : upLimit;
      setCount(start)
    }

    //終了時間セット
    const setEnd = (upLimit:number, downLimit:number, direction:string) => {
      const end = (direction==="up") ? upLimit : downLimit;
      setTimeEnd(end);
    }

    //タイマースタート
    const startCount = (upLimit:number=UPLIMIT, downLimit:number=DOWNLIMIT, direction:string=DIRECTION) => {
      console.log('start count');
      setStart(upLimit, downLimit, direction);//開始時間セット
      setEnd(upLimit, downLimit, direction);//終了時間セット
      setDirection(direction);//カウントアップ("up") or カウントダウン("down")をセット
      setIsTimerEnd(false);//カウント終了フラグをfalse
      setTimerFlag(true);//タイマー開始
    };

    const initCount = () => {
      console.log('init count!');
      setIsTimerEnd(false);
      setCount(0);
    }

    return { count, isTimerEnd , initCount, startCount };
};

export default useCounter;
```