FullStackOpen Part7-b Custom hooks メモ

Hooks

useStateやuseEffectなど。
使う上で以下のルールを守る

フックは:
ループ中・IF文・入れ子の関数中で呼ばない
Reactコンポーネント中・カスタムフック中から呼ぶこと

Custom hooks

カスタムフックとはコンポーネントのロジックを別の場所でも再利用するための機能
上記のルールに加えて、フックの名前はuseから始まる必要がある

例えば以下のようなカウンターがあるとする

import { useState } from 'react'
const App = () => {
  const [counter, setCounter] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>
        plus
      </button>
      <button onClick={() => setCounter(counter - 1)}>
        minus
      </button>      
      <button onClick={() => setCounter(0)}>
        zero
      </button>
    </div>
  )
}

これをカスタムフックuseCounterにすると以下のようになる

const useCounter = () => {
  const [value, setValue] = useState(0)

  const increase = () => {
    setValue(value + 1)
  }

  const decrease = () => {
    setValue(value - 1)
  }

  const zero = () => {
    setValue(0)
  }

  return {
    value, 
    increase,
    decrease,
    zero
  }
}

Reactコンポーネントをカスタムフックを使って書き直すと以下のようになる

const App = () => {
  const counter = useCounter()

  return (
    <div>
      <div>{counter.value}</div>
      <button onClick={counter.increase}>
        plus
      </button>
      <button onClick={counter.decrease}>
        minus
      </button>      
      <button onClick={counter.zero}>
        zero
      </button>
    </div>
  )
}

カウンター機能をカスタムフックに独立させたので、以下のように使いまわしすることもできる

const App = () => {
  const left = useCounter()
  const right = useCounter()

  return (
    <div>
      {left.value}
      <button onClick={left.increase}>
        left
      </button>
      <button onClick={right.increase}>
        right
      </button>
      {right.value}
    </div>
  )
}

その他にはフォームの入力項目をすべて一つ一つuseStateしていた以下のコードであれば

const App = () => {
  const [name, setName] = useState('')
  const [born, setBorn] = useState('')
  const [height, setHeight] = useState('')

  return (
    <div>
      <form>
        name: 
        <input
          type='text'
          value={name}
          onChange={(event) => setName(event.target.value)} 
        /> 
        <br/> 
        birthdate:
        <input
          type='date'
          value={born}
          onChange={(event) => setBorn(event.target.value)}
        />
        <br /> 
        height:
        <input
          type='number'
          value={height}
          onChange={(event) => setHeight(event.target.value)}
        />
      </form>
      <div>
        {name} {born} {height} 
      </div>
    </div>
  )
}

以下のようにカスタムフックとコンポーネントを書き換えられる

const useField = (type) => {
  const [value, setValue] = useState('')

  const onChange = (event) => {
    setValue(event.target.value)
  }

  return {
    type,
    value,
    onChange
  }
}
const App = () => {
  const name = useField('text')
  // ...

  return (
    <div>
      <form>
        <input
          type={name.type}
          value={name.value}
          onChange={name.onChange} 
        /> 
        // ...
      </form>
    </div>
  )
}

Spread attributes

先ほどのnameとuseFieldの例で考えると、input部分はスプレッド構文{…name}で記述できる

const App = () => {
  const name = useField('text')
  const born = useField('date')
  const height = useField('number')

  return (
    <div>
      <form>
        name: 
        <input  {...name} /> 
        <br/> 
        birthdate:
        <input {...born} />
        <br /> 
        height:
        <input {...height} />
      </form>
      <div>
        {name.value} {born.value} {height.value}
      </div>
    </div>
  )
}

可読性においても機能の独立という面でもカスタムフックは優れている

More about hooks

Ready-madeなカスタムフックはいろんなところに転がっているので活用しよう

Awesome React Hooks Resources on Github

演習の気づき

モジュールのエクスポートにはデフォルトエクスポートとネームドエクスポートの二種類ある

デフォルトエクスポート:
export default App
ネームドエクスポート:
export const useField = (type) => {
//…
}

カスタムフックの中身にアクセスするときはこんな感じ。
あくまでauthorはオブジェクトなので注意

    props.addNew({
      content: content.value,
      author: author.value,
      info: info.value,
      votes: 0
    })

Spread Syntaxで必要ないプロパティを除きたいときは以下のようにすればよい。
例えばresetまでInputタグに渡したくない場合は、useFieldを呼ぶ時点で分けておく

const CreateNew = (props) => {
  const {reset: resetContent, ...content} = useField('text')
  const {reset: resetAuthor, ...author} = useField('text')
  const {reset: resetInfo, ...info} = useField('text')

  const handleSubmit = (e) => {
    e.preventDefault()
    props.addNew({
      content: content.value,
      author: author.value,
      info: info.value,
      votes: 0
    })
  }

  const resetField = (e) => {
    e.preventDefault()
    resetContent()
    resetAuthor()
    resetInfo()
  }

演習7.7はこんな感じ
useEffect中は直接asyncできないので、一度asycn関数を定義してから呼び出す

  const useCountry = (name) => {
  const [country, setCountry] = useState(null)
  const [found, setFound] = useState(false)

  useEffect(() => {
    const fetchCountry = async () => {
      try {
        const response = await axios.get(`https://studies.cs.helsinki.fi/restcountries/api/name/${name}`)
        setCountry(response)
        setFound(true)
      } catch (exception) {
        setCountry(null)
        setFound(false)
      }
    }
    fetchCountry()
  }, [name])

  return {
    found,
    ...country
  }

演習7.8はこんな感じ。
新しいカスタムフックuseResourceを設定して、noteServiceとpersonServiceを一つにまとめている

import { useState, useEffect } from 'react'
import axios from 'axios'

const useField = (type) => {
  const [value, setValue] = useState('')

  const onChange = (event) => {
    setValue(event.target.value)
  }

  return {
    type,
    value,
    onChange
  }
}

const useResource = (baseUrl) => {
  const [resources, setResources] = useState([])

  const getAll = async () => {
    const response = await axios.get(baseUrl)
    setResources(response.data)
  }

  const create = async (resource) => {
    try {
      const response = await axios.post(baseUrl, resource)
      setResources(resources.concat(response.data))
    }
    catch (exception) {
      console.log(exception)
    }
  }

  const service = {
    getAll,
    create
  }

  return [
    resources, service
  ]
}

const App = () => {
  const content = useField('text')
  const name = useField('text')
  const number = useField('text')

  const [notes, noteService] = useResource('http://localhost:3005/notes')
  const [persons, personService] = useResource('http://localhost:3005/persons')

  const handleNoteSubmit = (event) => {
    event.preventDefault()
    noteService.create({ content: content.value })
  }

  const handlePersonSubmit = (event) => {
    event.preventDefault()
    personService.create({ name: name.value, number: number.value })
  }

  useEffect(() => {
    noteService.getAll()
  },)

  useEffect(() => {
    personService.getAll()
  },)

  return (
    <div>
      <h2>notes</h2>
      <form onSubmit={handleNoteSubmit}>
        <input {...content} />
        <button>create</button>
      </form>
      {notes.map(n => <p key={n.id}>{n.content}</p>)}

      <h2>persons</h2>
      <form onSubmit={handlePersonSubmit}>
        name <input {...name} /> <br />
        number <input {...number} />
        <button>create</button>
      </form>
      {persons.map(n => <p key={n.id}>{n.name} {n.number}</p>)}
    </div>
  )
}

export default App


この記事が気に入ったらサポートをしてみませんか?