5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

reduxをcustom hooksと使って関心の分離(SoC)とロジックの再利用をしたい

Last updated at Posted at 2020-06-16

はじめに

  • こちらの記事を大いに参考にしてます。
  • ある程度Reactとreduxに関しての知識がある読者を想定しております。
  • Reactにおいての関心の分離については賛否両論ありますのであくまで一案であるということで。
  • 筆者はredux歴がたったの二週間(執筆時点)なので頭おかしいこと書いてる可能性が大いにあります。温かい目で見て、よろしければコメントでご指摘ください。

やりたいこと

  • reduxと密に関係するロジックのほとんどをcustom hookとして別ファイルに置いといて他コンポーネントでも使えるようにしたい。
  • コンポーネント内でdispatchの文字を何度も見たくない。dispatchを呼び出すのはcustom hooks内からだけにしたい。

開発環境

  • TypeScript(3.7.2)
  • react(16.12.0)
  • redux(4.0.5)
  • react-redux(7.2.0)

使わないもの

Middleware

Middlewareの詳しい説明は省きますが(なにせ自分自身がよく理解できてないので)簡単に言うとreduxを使う上で非同期処理の扱いを助けてくれるやつです。そんな便利そうなものを使わずにどう非同期処理を扱うのかというと、custom hookに丸投げします。

Action creator関数

TypeScriptの型定義で事足りると思い使わないことにしました。
Action creatorの中でやりたい処理に関しては、custom hooksに丸投げします(二回目)。

###Redux ToolKit
便利そうですがまだ学習してないので今回はスルー。

丸投げってなんやねん

ごもっともです。これからその丸投げの中身を、おなじみカウンターの例を用いて解説していきたいと思います。

storeの設定

import { createStore } from 'redux'

type State = {
  count: number
}

type Increment = {
  type: 'INCREMENT'
}

type Decrement = {
  type: 'DECREMENT'
}

type CounterActions = Increment | Decrement

const initialState:State = {
  count: 0,
}

const reducer = (state = initialState, action: CounterActions) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }

    case 'DECREMENT':
      return { ...state, count: state.count - 1 }

    default:
      return state
  }
}

export default createStore(reducer)

actionの型をCounterActionsに定義した事によって、'INCREMENT''DECREMENT'以外の値をtypeプロパティに使おうとした場合エラーを吐いてくれます。

##custom hooks
###useCounter

import { useDispatch } from 'react-redux'
import { Dispatch } from 'react'

import { CounterActions } from '../redux/action'

const useCounter = () => {
  const dispatch = useDispatch<Dispatch<CounterActions>>()
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
  }
}

export default useCounter

dispatchはcustom hook内で呼び出しているのでコンポーネント側からはただincrement()と呼ぶだけです。

普段action creator関数内で行う処理もこれらの関数の中で行います。

機能追加もここの関数をいじるだけで簡単にできてうれしい。
###useAsyncCounter

import { useDispatch } from 'react-redux'
import { Dispatch } from 'react'

import { CounterActions } from '../redux/action'

const useAsyncCounter = () => {
  const dispatch = useDispatch<Dispatch<CounterActions>>()
  return {
    incrementAsync: () => {
      setTimeout(() => dispatch({ type: 'INCREMENT' }), 3000)
    },
    decrementAsync: () => {
      setTimeout(() => dispatch({ type: 'DECREMENT' }), 3000)
    },
  }
}

export default useAsyncCounter

非同期処理もこれらの関数内で行います。

##コンポーネント
###App

import React from 'react'
import { Provider } from 'react-redux'

import store from './store'
import CounterDisplay from './components/CounterDisplay'
import CounterButtons from './components/CounterButtons'

const App = () => {
  return (
    <Provider store={store}>
      <div id='app'>
        <CounterDisplay />
        <CounterButtons />
      </div>
    </Provider>
  )
}
export default App

###CounterDisplay

import React from 'react'
import { useSelector } from 'react-redux'

import { State } from '../redux/action'

const CounterDisplay = () => {
  const count = useSelector((state: State) => state.count)
  return <div id='display'>{count}</div>
}

export default CounterDisplay

###CounterButtons

import React from 'react'

import useCounter from '../hooks/useCounter'
import useAsyncCounter from '../hooks/useAsyncCounter'

const CounterButtons = () => {
  const { increment, decrement } = useCounter()
  const { incrementAsync, decrementAsync } = useAsyncCounter()

  return (
    <div id='buttons'>
      <button className='increment' onClick={() => increment()}>
        +
      </button>
      <button className='decrement' onClick={() => decrement()}>
        -
      </button>
      <button className='increment' onClick={() => incrementAsync()}>
        async +
      </button>
      <button className='decrement' onClick={() => decrementAsync()}>
        async -
      </button>
    </div>
  )
}

export default CounterButtons

onClickの中身がスッキリしてて、Foo↑気持ちぃ~
##おわりに

この手法を使ってて気になったのが、custom hookから返された関数をuseEffect内で使った際に意図しない形で何度も呼ばれてしまったりすることですが、これらはcustom hook内で返す関数をuseCallbackに包んであげればほぼ解決します。

最初はconnectやthunkを使って書いていたのですが、ボイラープレートの多さと、HOCによるコンポーネントツリーの汚染、コンポーネントファイルにいらんコードが増えるのにモヤッとしたので代替案を探してみました。

Redux ToolKitもこれらの問題を解決するのに役立ちそうなのでそろそろ手を出してみようとおもいます。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?