Restoration / ReactHooksJP

Describe About React Hooks in Japanese

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ReactHooks 日本語ドキュメント

React

目次

はじめに

React Hooksについて取り上げたリポジトリになります。2019年6月現在では、React Hooksに関して、あまり日本語の情報がなかったため日本語で情報配信をするためにもここに書いていきます。公式とは一切関係なく、あくまで自分が勉強した内容を記載しています。もしも間違っている箇所や認識が違っていたりしたら気軽にPRを送っていただければと思います。

またリポジトリだけでなくWikiにもコードを記載しているので、コードだけ読みたい方はこちら

ReactHooksとは

Version 16.8から追加された新機能。ざっくりと言えば、関数でもステート管理ができるようになった。
従来のReactのfunction componentはステートレスな関数でstate管理ができなかった。
なのでstate管理をしたい場合は、class componentを使う必要があったがfunction componentでも使えるようになった。
基本的なコンセプトは変わっていないので、props, state, context, refs, and lifecycleはHooksで書いても使えます。

公式ドキュメント

React Today and Tomorrow and 90% Cleaner React With Hooks
動画の31:14秒あたりのコードが参考になります。

使うメリット

  • 関数での記述で細かく分解できるのでコンポーネントの肥大化を防ぎ、テストがしやすくなる
  • 複雑なデザインパターンをしなくてもよくなる(render propsやHOC)
  • コード量がclassに比べて少ない、classよりも綺麗に書ける

Hooksを使用することでコンポーネント内のロジックを再利用可能な独立した単位としてまとめることができる。つまり、Hooksを使用することでReact本来の**(明示的なデータフローと構成)に近づく。

また、クラスやHOC、render propsの代わりに常に関数を使用するので コードの書き方も関数で統一されて規則的になる。 そのため複雑化しないので見通しがよくなります。

どのように使うのか?

まず基本となるHooks関数を理解する必要があります。
ポイントとして抑えておくべきは以下の3つ

他のAPIについては公式サイトを参照してください
Hooks API Reference

useState

関数内で状態管理をするために必要なHook関数になります、useStateの引数でstateを定義しており、引数に渡された値が初期値にあたります。 classコンポーネントでいうコンストラクタ内で定義するthis.stateにあたります。
useStateとthis.stateを比較したコードはコチラ

次に、useStateは関数に含まれている現在の値と更新する関数の2つを返します、この2つはペアで返ってきます。

import React, { useState } from 'react';

function Example() {
  // const = [現在の値, 更新用の関数] = useState(初期値);
  const [count, setCount] = useState(0);

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

useEffect

ライフサイクルメソッドのComponentDidMountにあたる関数で、デフォルトでは常にレンダー終了時に関数を実行させています。
オプショナルとして第二引数には変数の配列を渡すことが可能で、第二引数を使った場合はuseEffectの関数は第二引数の値が変更されたタイミングで実行されます。
また、第二引数は空の配列を渡すことも可能です、もし空配列を渡した場合は一回だけ実行されます。 useEffect内ではループが実行されています

// 第二引数なし
useEffect(() => {
  console.log('常にレンダー終了後に実行');
});

// 第二引数あり
useEffect(() => {
  console.log('paramで渡された値が変更されたとき実行');
}, [param]);

// 空で渡した場合
useEffect(() => {
  console.log('一回だけ実行');
}, []);

useContext

useContextはContextの値を受け取るための関数になります。 例えば、Providerから渡された子コンポーネントはuseContextを使用することで値を受け取ることが可能になります。

import SomthingContext from "somthingContext";
const ctx = useContext(SomthingContext);
console.log(ctx); // somthingContextで定義された値を受け取ることができる

Hooksにおけるルール

Hooksを使用する上で気をつけないといけないポイントがあります。以下の3つのルールがあります。

  • Hooksを呼び出すのはトップレベルのみ、つまりrenderなどでの呼び出しは不可
  • ネストした関数やループ内、if文での条件で呼び出すことは不可
  • Hooksが呼び出せるのはReactFunction、つまりクラスでの呼び出しや標準のJavaScript関数では不可

ESLintによるチェックの追加

公式サイトより引用

$ npm install eslint-plugin-react-hooks --save-dev
// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

ContextAPIとは

Context
Context APIはReactにおいては標準搭載なので外部のプラグイン等のインストールは不要です。Context APIとはいわゆる グローバルで扱えるprops に当たります。従来のpropsではコンポーネントごとに渡す必要がありましたが、Context APIを使用した場合は、以下のようにどこからでも呼び出しが可能となります。

以下の図を見てもらえば理解しやすいです。 引用元
Context API

ContextAPIの使用例

Contextを定義、Context.Providerで値を渡す、ContextConsumerで値を受け取る、この流れで行います。 以下のコードは上記のフローに加えて、親コンポーネントからContextを子コンポーネントに渡し、親コンポーネントのステータスを変更するコードになります。

従来のClassを使用

import React from 'react';
import logo from './logo.svg';
import './App.css';

// set the defaults
const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
});

class LanguageSwitcher extends React.Component {
  render() {
    return (
      <LanguageContext.Consumer>
        {({ language, setLanguage }) => (
          <button onClick={() => setLanguage("jp")}>
            Switch Language (Current: {language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}
class App extends React.Component {
  setLanguage = language => {
    this.setState({ language });
  };

  state = {
    language: "en",
    setLanguage: this.setLanguage
  };

  render() {
    return (
      <LanguageContext.Provider value={this.state}>
        <h2>Current Language: {this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          {/* Can be nested */}
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}

export default App;

簡単な例

import React from 'react';
import logo from './logo.svg';
import './App.css';

// set the defaults
const LanguageContext = React.createContext({
  language: "en",
});

const LanguageSwitcher = () => {
  return (
    <LanguageContext.Consumer>
      {({ state, setState }) => (
        <button onClick={() => {setState("jp")} }>
          Switch Language (Current: {state})
        </button>
      )}
    </LanguageContext.Consumer>
  );
}
const App = () => {
  const [state, setState] = React.useState("en");
  return (
    <LanguageContext.Provider value={ {state,setState} }>
      <h2>Current Language: {state}</h2>
      <p>Click button to change to jp</p>
      <div>
        <LanguageSwitcher />
      </div>
    </LanguageContext.Provider>
  );
}
export default App;

従来のコードとの比較

appディレクトリに実際に比較したコードが入っているので確認してください。

$ git clone https://github.com/Restoration/ReactHooksJP
$ cd ReactHooksJP/app
$ npm i
$ npm start

マウスの位置情報を取得するコードをHOCとrender propsで書き、Hooksで代用してみます。 参照元はコチラ

HOC

import React from 'react';

function withMousePosition(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { x: 0, y: 0 };
    }

    componentDidMount() {
      window.addEventListener("mousemove", this.handleMouseMove);
    }

    componentWillUnmount() {
      window.removeEventListener("mousemove", this.handleMouseMove);
    }

    handleMouseMove = event => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      });
    };

    render() {
      return (
        <WrappedComponent
          {...this.props}
          mousePosition={this.state}
        />
      );
    }
  };
}
export default withMousePosition;
import React from "react";
import withMousePosition from "./withMousePosition";

function App(props) {
  const { x, y } = props.mousePosition;

  return (
    <div className="App">
      <h1>Higher-Order Component Method</h1>
      <h2>Move the mouse around!</h2>
      <p style={{ background: "orange" }}>
        The current mouse position is ({x}, {y})
      </p>
    </div>
  );
}
export default withMousePosition(App);

render props

import React from "react";

class MousePosition extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

  componentDidMount() {
    window.addEventListener("mousemove", this.handleMouseMove);
  }

  componentWillUnmount() {
    window.removeEventListener("mousemove", this.handleMouseMove);
  }

  handleMouseMove = event => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  };

  render() {
    return (
      <div
        style={{ height: "100%", width: "100%" }}
        onMouseMove={this.handleMouseMove}
      >
        {this.props.render(this.state)}
      </div>
    );
  }
}

export default MousePosition;
import React from "react";
import MousePosition from "./MousePosition";

function App() {
  return (
    <div className="App">
      <h1>Render Props Method</h1>
      <h2>Move the mouse around!</h2>
      <MousePosition
        render={mousePosition => (
          <p style={{ background: "skyblue" }}>
            The current mouse position is ({mousePosition.x}, {mousePosition.y})
          </p>
        )}
      />
    </div>
  );
}

export default App;

Hooks

import React, { useState, useEffect } from 'react';

// Using Hooks
function useMousePosition() {
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

  function handleMouseMove(event) {
    setMousePosition({
      x: event.clientX,
      y: event.clientY
    });
  }

  useEffect(() => {
    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, []);

  return mousePosition;
}

export default useMousePosition;
import React from "react";
import useMousePosition from "./useMousePosition";

function App() {
  const { x, y } = useMousePosition();

  return (
    <div className="App">
      <h1>React Hook Method</h1>
      <h2>Move the mouse around!</h2>
      <p style={{ background: "palegreen" }}>
        The current mouse position is ({x}, {y})
      </p>
    </div>
  );
}
export default App;

Redux

Reduxの代用はできるのか?結論からしてReduxの代用にはなる。ただし、もともとのコンセプトが違う。 HooksとContext APIを使用してReduxのような動きをさせるというものになる。 なのでReduxを使うのかReactHooks+Context APIによる実装でReduxの代用するかはプロジェクトに依存する。 現状ステートの一元管理をするのであれば以下の3パターンが考えられる

まずは、Hooksを使用した最小構成のReduxコードを見てみましょう。
Reduxのnpmパッケージは含まず、HooksだけでReduxの動きを完結させます。

// useReducerはReact内に含まれます
import React, { useReducer } from 'react';

const initialState = 0;
const reducer = (state, action) => {
  switch (action) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'reset': return 0;
    default: throw new Error('Unexpected action');
  }
};

const  App = () => {
  // const [現在の値, 更新用関数] = useReducer(reducer関数, 初期値);
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {count}
      <button onClick={() => dispatch('increment')}>+1</button>
      <button onClick={() => dispatch('decrement')}>-1</button>
      <button onClick={() => dispatch('reset')}>reset</button>
    </div>
  );
};

export default App;

複数に対して

import React, { useReducer } from 'react';

const initialState = 0;
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'set': return action.count;
    default: throw new Error('Unexpected action');
  }
};

const App = () => {
  // 処理は一緒なのでここでフックさせてる
  const [count1, dispatch1] = useReducer(reducer, initialState);
  const [count2, dispatch2] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        {count1}
        <button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
      </div>
      <div>
        {count2}
        <button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
        <button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
        <button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
      </div>
    </>
  );
};

export default App;

参考リンク

Author

RyotArch

About

Describe About React Hooks in Japanese


Languages

Language:JavaScript 47.9%Language:TypeScript 32.4%Language:HTML 13.0%Language:CSS 6.8%