React HooksとRedux Hooksを使ってみた

はじめに

React Hooksについて少し学んでみました。

追記:Redux Hooksも試しました。

✅参考

💻フックの導入(公式)

導入経緯など

💻フック早わかり(公式)

ステートフックなど詳細な説明は次のページ以降に記載されている。

💻5分でわかるReact Hooks

とてもわかりやすい。

💻🎉React 16.8: 正式版となったReact Hooksを今さら総ざらいする

たくさんフックの種類を紹介してくれている。

💻最近Reactを始めた人向けのReact Hooks入門

上記に加えて勉強する際読みたい。

💻useReducerフック活用法

Reduxを理解する必要がなくなる??
⇒Reduxが本当に必要なケースは減って行く(ReduxはContextとuseReducerの結合以上のことを行うため)。

💻Redux Hooks によるラクラク dispatch & states

これRedux Hooksに関しては一番わかりやすい。
※公式が英文しかないので非常に助かった。

Reactを学んでみた

Reactの関数型コンポーネントについて

React Hooks

長期的には、フックが React のコンポーネントを書く際の第一選択となることを期待しています。
フック、クラスのいずれを使うべきですか、あるいはその両方でしょうか? [公式ドキュメント]

✅フックとは

関数コンポーネントから使える新しいAPI。状態管理などをクラスを書かずしてできる。

⇒関数コンポーネントでも、フックの登場によって状態管理やライフサイクルの機能が使えるようになった◎

※要するにReduxのconnect()を利用しなくても、各コンポーネントとdispatch, stateが簡便に利用できるようになった。

✅基本フック

useState 参考

クラスコンポーネント(でいうsetState)を頼らずにstateを持つことができる。

const [stateの値, state更新関数] = useState(state初期値);

useEffect 参考

レンダリングの後に処理を動作させることができる。

従来のReactでいう、componentDidMount componentDidUpdateと同じように使える。

useContext 参考

propsで親→孫へ直接データの受け渡しが可能になる。

useReducer 参考

useStateより複雑なstateを管理するのに役立つ。Reduxの代替になる。
ステートの初期状態に加えreducerと呼ばれる関数を渡すので、現在のステートに加えて、ステートを更新する関数の代わりにdispatch関数が得られる。

🙄ここから急に複雑になったので、実際に書きながら覚えていこう。

※Reducerとは、2つの値を取り、1つの値を返す関数のこと。

useRefフック 参考

DOMノードへの永続的な参照を作成することが可能。

使ってみた

✅useStateフック

// useStateフックをインポート
import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  const [count, setCount] = useState(1);
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> // 無名関数(即実行)
        Click me
      </button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'));

※フックを使わない場合

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </>
    )
  }
}

export default App

🙄クラス型はsetStateの中身含めて冗長。書く気が無くなります。。。

ためしに変数の中身を見てみる。

const App = () => {
  const [count, setCount] = useState(1);
  console.log(count); // add
  console.log(setCount); // add
  console.log(useState); // add
  console.log(useState(1)); // add
  return (

useState(1)が2つの持った配列を返すことがわかります。

(2) [1, ƒ]

たとえばuseState(5)にするとこんな感じ。

(2) [5, ƒ]

これでcountに1を、setCountにf関数を代入しているのがわかりました。

👉useStateでは1つ目の引数に描画用、2つ目の引数に更新用(関数が入る)の変数を用意する。

※余談※

クラス型の中身を見てみる。

render() {
  console.log(this.props);
  console.log(this.state);
  console.log(this.setState);

✅useEffectフック

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [count, setCount] = useState(0);
  React.useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

クリックに合わせてタイトルが変わる。

✅useContextフック

import React from 'react';
import ReactDOM from 'react-dom';

const Context = React.createContext()

const Mago = () => {
  const { money } = React.useContext(Context)
  return <p>{money}円</p>
}

const Kodomo = () => <Mago />

const Oya = () => {
  return (
    <Context.Provider value={{ money: 10000 }}>
      <Kodomo />
    </Context.Provider>
  )
}

ReactDOM.render(<Oya />, document.getElementById('root'));

※使わない場合

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const Mago = ({money}) => <p>{money}円</p>

const Kodomo = ({money}) => <Mago money={money} />

const Oya = () => {
  const [money] = useState(10000)
  return (
    <Kodomo money={money} />
  )
}

ReactDOM.render(<Oya />, document.getElementById('root'));

🙄小規模ならいらないかも??それとuseStateとかもReact.useStateとすればインポート不要なことに今更気づいた。

✅useReducerフック

参考:useReducerフック活用法

第1引数でレデューサー、第2引数で初期値を設定する。

import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';

function Counter() {
  const [sum, dispatch] = useReducer((state, action) => {
    return state + action;
  }, 0);

  return (
    <>
      <p>{ sum }</p>
      <button onClick={() => dispatch(1)}>
        Add
      </button>
    </>
  );
}

ReactDOM.render(<Counter />, document.getElementById('root'));

中身を見てみる

}, 0);
console.log(useReducer());
console.log(sum);
console.log(dispatch(1));
return (

dispatch()メソッドを出力すると何回もレンダリングされてしまうのでエラーになるが、1ずつsumに加算されているのがわかる。

🙄ただこのくらいの実装ならuseState使えばいい気がする。この次にuseRefを活用した複雑な例も登場するが一旦ステイ。

✅useReducerでReduxの代替

参考:🎉React 16.8: 正式版となったReact Hooksを今さら総ざらいする

引用:useReducerは useState の亜種であり、ステートを宣言するフックです。useReducerは、ステートの初期状態に加えてreducer と呼ばれる関数を渡します。結果として、現在のステートに加えて、ステートを更新する関数の代わりに dispatch 関数が得られます。

🙄この記事のサンプルでやっと恩恵を理解できた気が。Redux使わずにカウンターアプリを作れる。

更に簡易的なカウンターアプリを作ってみる。

作れた◎

上記のUseReducerSampleコンポーネントとControlButtonsコンポーネントをまとめると。

import React, { useReducer } from "react";
import ReactDOM from "react-dom";

const reducer = (num , action) => {
  //eslint-disable-next-line
  switch (action) { 
    case 'increment':
      return num + 1;
    case 'decrement':
      return num - 1;
  }
};

export const Counter = () => {
  const [num, dispatch] = useReducer(reducer,0);
  return (
    <div>
      <p>クリック:{num}</p>
      <p>
        <button onClick={() => dispatch('decrement')}>-</button>
        <button onClick={() => dispatch('increment')}>+</button>
      </p>
    </div>
  );
};

ReactDOM.render(<Counter />, document.querySelector("#root"));

🙄だいぶコードの理解がしやすくなりました◎

ただカウンターアプリならuseStateで作れることに公式読んでて気づきました。笑

import React, { useReducer } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(n => n - 1)}>-</button>
      <button onClick={() => setCount(n => n + 1)}>+</button>
    </>
  );
}
ReactDOM.render(<Counter />, document.querySelector("#root"));

🙄そもそもカウンターアプリならsetStateで、Redux使う必要すらなかったですね。。。

以下公式引用:

useReducerがuseStateより好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、前のstateに基づいて次のstateを決める必要がある場合です。

✅useReducer使って2つの値を更新する

const reducer = ({num} , action) => {
  //eslint-disable-next-line
  switch (action) { 
    case 'increment':
      return {num:num + 1, message:`${num}に+1しました`}
    case 'decrement':
      return {num:num - 1, message:`${num}に-1しました`}
  }
};

export const Counter = () => {
  const [state, dispatch] = useReducer(reducer,{
    num: 0,
    message: "クリックしてください"
  });
  return (
    <div>
      <p>カウンターアプリ</p>
      <p>{state.message}</p>
      <p>クリック:{state.num}</p>
      <p>
        <button onClick={() => dispatch('decrement')}>-</button>
        <button onClick={() => dispatch('increment')}>+</button>
      </p>
    </div>
  );
};

わかりやすくステートを作る。

const initData = {
  num: 0,
  message: "クリックしてください"
}

const reducer = ({num} , action) => {
  // eslint-disable-next-line
  switch (action) { 
    case 'increment':
      return {num:num + 1, message:`${num}に+1しました`}
    case 'decrement':
      return {num:num - 1, message:`${num}に-1しました`}
  }
};

export const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initData);
  return (
    <div>
      <p>カウンターアプリ</p>
      <p>{state.message}</p>
      <p>クリック:{state.num}</p>
      <p>
        <button onClick={() => dispatch('decrement')}>-</button>
        <button onClick={() => dispatch('increment')}>+</button>
      </p>
    </div>
  );
};

🙄ステートを作ったほうがuseReducerの引数に変数を入れれるのでわかりやすい。

リセットボタンの追加。

const initData = {
  num: 0,
  message: "クリックしてください"
}

const reducer = ({num} , action) => {
  // eslint-disable-next-line
  switch (action) { 
    case 'increment':
      return {num:num + 1, message:`${num}に+1しました`}
    case 'decrement':
      return {num:num - 1, message:`${num}に-1しました`}
    default:
      return initData
  }
};

export const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initData);
  return (
    <div>
      <p>カウンターアプリ</p>
      <p>{state.message}</p>
      <p>クリック:{state.num}</p>
      <p>
        <button onClick={() => dispatch('decrement')}>-</button>
        <button onClick={() => dispatch('increment')}>+</button>
        <button onClick={() => dispatch()}>reset</button>
      </p>
    </div>
  );
};

🙄reducerの引数にinitDataを指定するとオブジェクトなので返り値をinitData.numとしないといけない。ただそうすると今度はリセットのinitDataが初期値に戻らなく誤作動が発生した。なので、引数には分割代入でnumのみ入れて実装。

ソースコード

📝メモ

useReducerを使う際、オブジェクトで初期値を設定した場合。

  const [state, dispatch] = useReducer(reducer, {count: 0});

state = {count: 0} となるので、JSXで表示させたい場合、state.countとなる。

正直わざわざオブジェクトに入れなくてもいいかなと思った。

オブジェクトを使った場合、レデューサーも返り値をオブジェクトにしなければいけない。

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

簡易的に書くなら、useReducerをこうして。

const [state, dispatch] = useReducer(reducer, 0);

レデューサーはこうする。

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
    default:
      throw new Error();
  }
}

🙄拡張する場合、オブジェクトがマスト。且つこういった1つの値を更新する場合はuseStateで代替できるため、公式サンプルとかもこういった書き方をしていないのかなと思った。

Redux Hooksを使ってみた

💻Redux Hooks によるラクラク dispatch & states

変わらずカウンターアプリで試してみました。※書籍React超入門がベース。

✅useDispatchとuseSelector

useDispatch:storeに紐付いたdispatchが取得可能。

※dispatchとは、reduxの状態を更新するために新しい状態を送ること。

useSelector:storeからstateを取得する場合に使用。useSelectorの引数にはstateを引数にとる selector関数を指定。

👇useSelectorを使う際の注意点

dispatch()実行されるたびに、(※propsが渡される前に)useSelector()は実行されるため、propsが最新の状態である保証がない

(対策)⇒selector関数ではpropsを利用しない。

✅使ってみる

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './App';

// ステートの値
let state_value = {
    counter:0,
    message:"COUNTER"
}

// レデューサー
function counter(state = state_value, action) {
  switch (action.type) {
      case 'INCREMENT':
        return increment(state);
      case 'DECREMENT':
        return decrement(state);
      default:
      return state;
  }
}

// レデュースアクション
function increment(state) {
  return {
    counter:state.counter + 1,
    message:"INCREMENT"
  }
}
function decrement(state) {
  return {
    counter:state.counter - 1,
    message:"DECREMENT"
  }
}

// ストアを作成
let store = createStore(counter);

// 表示をレンダリング
ReactDOM.render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('root')
);

App.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import './App.css';

const counterSelector = state =>  state ;

// Appコンポーネント
let App = () => {
  // useDispatchでstoreに紐付いたdispatch取得。
  const dispatch = useDispatch();
  // useSelectorでstoreのstate取得。
  const {counter, message} = useSelector(counterSelector);
  let doAction = e => {
    if (e.shiftKey){
      dispatch({ type:'DECREMENT' });
    } else {
      dispatch({ type:'INCREMENT' });
    }
  }
  return (
    <div>
      <p>{message}: {counter}</p>
      <button onClick={doAction}>
        click
      </button>
    </div>
  );
}

export default App;

🙄App.js側でdispatchの引数がオブジェクトなのが微妙なので関数に使用と思いきや、それはそれでimportやらindex.jsでもないと困るやら手間がかかったのでやめました。

多分これが一番シンプルな実装かと。

教本ではAppコンポーネントに加えてButton,Messageコンポーネントもあるけど今回は統一しました。

ただRedux使わずReact Hooksのみで実装したカウンターアプリの方が圧倒的にわかりやすいですね。笑

臨機応変に使っていこう。

まとめ

最後に、最終的に落ち着いたReact Hooksのみのコード。これが一番シンプル。

index.js(useState)のみ。

import React, { useReducer } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(n => n - 1)}>-</button>
      <button onClick={() => setCount(n => n + 1)}>+</button>
    </>
  );
}
ReactDOM.render(<Counter />, document.querySelector("#root"));

index.js(useReducer)のみ。

import React, { useReducer } from "react";
import ReactDOM from "react-dom";

const initData = {
  num: 0,
  message: "クリックしてください"
}

const reducer = ({num} , action) => {
  // eslint-disable-next-line
  switch (action) { 
    case 'increment':
      return {num:num + 1, message:`${num}に+1しました`}
    case 'decrement':
      return {num:num - 1, message:`${num}に-1しました`}
    default:
      return initData
  }
};

export const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initData);
  return (
    <div>
      <p>カウンターアプリ</p>
      <p>{state.message}</p>
      <p>クリック:{state.num}</p>
      <p>
        <button onClick={() => dispatch('decrement')}>-</button>
        <button onClick={() => dispatch('increment')}>+</button>
        <button onClick={() => dispatch()}>reset</button>
      </p>
    </div>
  );
};

ReactDOM.render(<Counter />, document.querySelector("#root"));

参考:React, Redux 初心者が、Hooks 時代の React, Redux, React-Redux に触れてみて感じたこと

🙄逆に上記記事では、大規模開発ではuseState,useReducerは使わないと書かれている。

useStateだと足し引きのロジックがコンポーネント内に含まれているため。

useReducerだとreducer関数の制約で、複数コンポーネントで値を共有するのが手間らしい。

ただ、Redux-hooksは活用すべしとのこと。

👉本当に開発の内容次第&好みの問題な気がしました。

おわりに

実際にコードを書きながら理解を深めていきたい。

とりあえずReduxの知識は必要であること(あって損はない)、propsをまずは使いこなせるようにしないといけないなと感じた。

※6/4追記

実際にコードを書きながら深掘りしてみた。

少しだけフックと仲良くなれてきた感。

今回みたいな簡易的なアプリ制作であればRedux(Redux Hooks含め)は不要だと感じた。

書籍のToDoアプリもRedux Hooksで試してみようかな。

参考:useReducerの本質:良いパフォーマンスのためのロジックとコンポーネント設計

上記記事を追って読みたい。

備忘録(躓いた点)

✅setState内に関数

useStateで2つの値を使ってみたところ以下の形で実装できた。

function Counter() {
  const [count, setCount] = useState({
    num:0,
    message:"クリックしてください"
  });

  function increment(n){
    return {num:n +1,message:`${n}に+1しました`};
  };

  function decrement(n){
    return {num:n -1,message:`${n}に-1しました`};
  };

  return (
    <>
      <p>{count.message}</p>
      Count: {count.num}
      <button onClick={() => setCount({
        num:0,
        message:"クリックしてください"
      })}>Reset</button>

      <button onClick={() => setCount(decrement(count.num))}>-</button>
      <button onClick={() => setCount(increment(count.num))}>+</button>
    </>
  );
}

🙄ちょっとわかりづらいコードですね。。。

ってことで以下の感じで無名関数を入れようとしたところエラーが出ました。

<button onClick={() => setCount(n => {num: n -1})>-</button>

※解決できず。

コメントを残す