20200124のReactに関する記事は4件です。

挫折しないで!作って学ぶ React + Redux 入門【実践】

はじめに

普段サーバサイドをメインでやっているのですが、つい最近プロジェクトでReactを使う機会があり、一度は挫折したReduxを使いました。
メモのつもりでしたが、
「現在Reactを触ってて、Reduxも使いたい」
「Reduxでやりたいことはなんとなく分かるけど使い方がイマイチ分からない」
って方の助けになれればと思い、React + Redux の使い方をざっとまとめました。

Reduxとは

Reactが扱うUIのstate(状態)を管理をするためのフレームワークです。
詳しくはここでは説明しませんが、色々ググれば詳しいことは書いてあります。

サンプル(イメージ)

今回は入力フォームにテキスト(名前)を入力して、それを出力するといったごく簡単なものです。
できる限りわかりやすいように簡単なものにしました。
demo.gif
デモページ

準備(インストール)

まず、create-react-app でReactアプリを作成します。
続いてreduxreact-redux をインストールします。

$ npm install --save redux react-redux

開発時にreduxのログを出力したい場合は、redux-loggerもインストールします。

$ npm install --save redux-logger

構成

src/
 ├── components/
 │       ├── ui/
 │       │    ├── SendName.jsx
 │       │    └── GetName.jsx
 │       └── App.jsx
 ├── redux/
 │     ├── reducers/
 │     │      ├── index.js
 │     │      └── name.js
 │     ├── action.js
 │     ├── actionTypes.js
 │     ├── selectors.js
 │     └── store.js
 └── index.js

Redux

1. Actions

Actions は、アプリケーションからstoreにデータを送信する情報のオブジェクトです。

まずはじめにActionのタイプの名前を定義します。

~/redux/actionTypes.js
export const SEND_NAME = 'SEND_NAME' 

アプリケーションが大きくなることを踏まえて、別ファイルに記述しています。
今回は一つだけですが、複数のActionがある場合は複数記述してください。
Actionのタイプはあなた次第です。

続いて実際にActionを定義します。

~/redux/action.js
import { SEND_NAME } from 'redux/actionTypes'

export const sendName = name => ({
  type: SEND_NAME,
  name: name
});

ここで注意なのがtype は必ず入れてください。
あとは自分が開発するアプリケーションに合わせて定義してください。
今回は入力される名前だけなのでname だけ定義しています。

2. Reducers

Reducers は、storeに送信されたActionsに応じてアプリケーションの状態がどのように変化するかを指定するメソッドを定義します。

まずはじめに初期状態を定義します。

const initialState = {
  name: ''
};

つづいてActionのタイプに応じて処理を書いていきます。
複数のActionを定義している場合はタイプに応じて処理を書いてください。

export default (state = initialState, action) => {
  switch (action.type) {
    // case Actionタイプ名
    case SEND_NAME:
      return {
        ...state,
        name: action.name
      };
    default:
      return state;
  }
}

まとめると下記のようになります。

~/reducers/name.js
import { SEND_NAME } from 'redux/actionTypes'

const initialState = {
  name: ''
};

export default (state = initialState, action) => {
  switch (action.type) {
    case SEND_NAME:
      return {
        ...state,
        name: action.name
      };
    default:
      return state;
  }
}

最後に定義したReducerを呼び出し一つのオブジェクトにします。
これもアプリケーションが大きくなることを踏まえてReducerを分けています。

~/reducers/index.js
import { combineReducers } from 'redux'
import name from 'redux/reducers/name'

export default combineReducers({name});

3. Stores

続いてStores を定義します。
Reduxではstoreは必ず一つにしてください。

import { createStore } from 'redux'
import rootReducer from 'redux/reducers'

export default createStore(rootReducer);

もしもログを出したい場合は下記のようにしてください。
今回は開発環境のみにログを出します。

~/redux/store.js
import { createStore, applyMiddleware } from 'redux'
import rootReducer from 'redux/reducers'
import { createLogger } from 'redux-logger'

const middlewares = [];

if(process.env.NODE_ENV !== 'production') {
  const logger = createLogger({
    diff: true,
    collapsed: true,
  });
  middlewares.push(logger);
}

const store = createStore(rootReducer, applyMiddleware(...middlewares));

export default store;

React + Redux

続いてReactからReduxを操作します。

書き込み

まずはじめに入力した値(名前)をReduxで宣言したname に書き込みます。
書き込む際は、

  • ReactとReduxを繋ぐ為のconnect
  • 使用したいAction

を読み込みます。

import { connect } from 'react-redux'
import { sendName } from 'redux/action'

続いてconnect を使いReactとReduxを繋ぎます。

connect(mapStateToProps, mapDispatchToProps)(App);
  • 第一引数mapStateToPropsは、componentに渡すpropsを制御します
  • 第二引数のmapDispatchToPropsは、reducerを呼び出して、reduxで管理しているstateを更新します
  • Appは、取得したデータをpropsとして扱いたいcomponentを指定します

では、さっそく書き込み用のJSXを作成します。

~/components/ui/SendName.jsx
import React, { Component }  from 'react'
import { connect } from 'react-redux'
import { sendName } from 'redux/action'

class SendName extends Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  updataName = name => {
    name = name.trim();
    this.setState({ name });
  };

  handleSendName = () => {    
    this.props.sendName(this.state.name);
    this.setState({name: ''});
  };

  render() {
    return (
      <div className={Style.form}>
        <label htmlFor='name'>
          名前
          <input type='text' onChange={e => this.updataName(e.target.value)} value={this.state.name}>
        </label>
        <button onClick={this.handleSendName}>送信</button>
      </div>
    );
  }
}

export default connect(null, {sendName})(SendName);

読み込み

まずはじめにReduxで書き込んだ値を取得する処理を書きます。

~/redux/selectors.js
export const getNameStore = store => store.name;

export const getName = store =>
  getNameStore(store) ? getNameStore(store).name : 'No Name';

続いてJSXを作成します。
こちらもconnectと、先ほど作成したファイルを読み込みます。
MapStateToPropsで取得する処理を書き、connectの第一引数で呼び出します。

~/components/ui/GetName.jsx
import React from 'react'
import { connect } from 'react-redux'
import { getName } from 'redux/selectors'

const GetName = ({name}) => {
  return (
    <p>入力された名前:{name}</p>
  );
}

const MapStateToProps = state => {
  const name = getName(state);
  return {name};
}

export default connect(MapStateToProps)(GetName);

これでReact + Reduxでstate(状態)を管理できます。

さいごに

Reduxは理解するまで少し大変だと思いますが、是非手を動かしながら学んでみてください。
Reduxが使えればReactでの開発がすごく変わります!
是非トライしてみましょ!

コード

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactメモ

はじめに

Reactを書いているときに、簡単だけど「どうやるんだっけ?」ってなることをまとめていきます。
自分用のメモといった感じです。

画像表示

import Image from './images/image.png'
const Component = () =>{
  return <img src={Icon}  alt="アイコン" />
}
export default Component
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネントのテスト設計と実装(後編)

はじめに

この記事ではReactコンポーネントのテストユーティリティであるReact Testing Libraryについて説明します。

この記事で説明すること

  • 前編
    • Testing Trophyの概要
    • React Testing Libraryのコンセプト
  • 後編(この記事)
    • Reactコンポーネントのテストケースの設計
    • React Testing Libraryを使用したテストコードの実装

開発環境

  • React: 16.12.0
  • Jest: 24.9.0
  • React Testing Library: 9.3.2

React Testing Libraryを使ってみようと思ったきっかけ

だいぶ前のことになるのですが、ReactコンポーネントのテストユーティリティとしてEnzymeに代えてReact Testing Libraryを使ってみました。

Enzymeを使っていたときは、find(#id)find(displayName)といったコードをよく書いていました。ただこのようなコードを書くと、id名やdisplayNameを変更するというリファクタリングをしただけでもテストコードを書き直す必要が出てきてしまいます。

これではリファクタリングのコストが大きすぎるということで、Enzymeで良い方法が無いかを探していたところ、むしろEnzymeに代わるものとしてReact Testing Libraryというライブラリがあることを知りましたのでこのライブラリを使用してみました。合わせて、React Testing Libraryの作者によりTesting Trophyという考え方が提唱されているということも知りました。

そこで、この記事では私がReact Testing Libraryを学習する中で知ったTesting TrophyとReact Testing Libraryの考え方と、この考え方を実際のテストコードに適用した例を説明します。

コンポーネント単体のテスト

まず最初にコンポーネント単体のテストについて考えます。

テストの方針

テストの方針としては、React Testing Libraryの開発者であるKent C. Dodds氏の言葉にあるように、propsと描画される結果に着目します。

so what parts of our code do each of these users use, see, and know about? The end user will see/interact with what we render in the render method. The developer will see/interact with the props they pass to the component. So our test should typically only see/interact with the props that are passed, and the rendered output.

また、ロジックがない部分については、単純な実装でありテストをする必要性が低いものであったり、型チェックなどができるため、コンポーネント単体のテストの対象からは除外します。

Things that really have no logic in them at all (so any bugs could be caught by ESLint and Flow). Maintaining tests like this actually really slow you and your team down.

つまり、コンポーネント単体のテストとしては、以下のようなコンポーネントをテストする方針とします。

  • テスト対象のコンポーネント
    • コンポーネント内で何らかのDOM要素を描画する際にロジックを含むもの
  • テストの内容
    • コンポーネントのpropsの値に応じてDOM要素が正しい内容で描画されるかどうか

テスト設計の手順

前述したテストの方針を踏まえて、以下の手順でテストケースの設計を行います。

  1. コンポーネントのpropsをリストアップする

  2. 各propsが取り得る値をリストアップする

    1. propsの初期状態がどのようになっているか
    2. 正常時にはpropsがどのような値を取り得るか
    3. エラー時にはpropsがどのような値を取り得るか
  3. 各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  4. それぞれの状態をテストケースとする

テスト設計とテストコードの実例

では、実際にテストケースの設計とテストコードの実装を行ってみます。

テスト対象のコードとして、Reduxのチュートリアルで使用されているTodoリストを使用します。

(本記事では、Todoリストアプリのコードについては説明はしませんので、コードの内容はReduxのチュートリアルページを見てください。また、Todoリストアプリの完成イメージはこちらで確認できます)

1つ目の例として、Todoコンポーネントのテスト設計をします。

1.コンポーネントのpropsをリストアップする
Todoコンポーネントが受け取るpropsはcompleted: booleantext: stringになります。

2.各propsが取り得る値をリストアップする

  • 初期状態: なし
  • 正常値:
    • completed = true, text = 'todo item'
    • completed=false, text='todo item'
  • エラー値: なし

3.各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  • 正常値
    • completed = true, text = 'todo item' → Todoコンポーネント内にlist要素があり、text-decoration: line-throughのstyleが適用されている
    • completed=false, text='todo item' → Todoコンポーネント内にlist要素があり、text-decoration: line-throughのstyleが適用されていない

4.それぞれの状態をテストケースとする
1〜3をまとめると以下のようになります。

(2)正常値のケース
(2-1)completed=true

(2-1)completed=false
props This This
 completed true false
 text 'todo item' 'todo item'
期待値
 list要素 あり あり
 style text-decoration: line-through text-decoration: none

実際のコードは以下のようになります。

import React from 'react';
import { render } from '@testing-library/react';

import Todo from './Todo';

describe('Todo component', () => {
  describe('(2-1)completed=true', () => {
    it('has list and line-through style', () => {
      const todo = 'todo item'
      const completed = true
      const onClickMock = jest.fn();

      const { getByText } = render(<Todo text={todo} completed={completed} onClick={onClickMock}/>);

      expect(getByText(todo)).toBeInTheDocument()
      expect(getByText(todo)).toHaveStyle('text-decoration: line-through;')
    })
  });

  describe('(2-1)completed=false', () => {
    it('has list and no line-through style', () => {
      const todo = 'todo item'
      const completed = false
      const onClickMock = jest.fn();

      const { getByText } = render(<Todo text={todo} completed={completed} onClick={onClickMock}/>);

      expect(getByText(todo)).toBeInTheDocument()
      expect(getByText(todo)).toHaveStyle('text-decoration: none;')
    })
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

2つ目の例として、TodoListコンポーネントのテスト設計をします。

1.コンポーネントのpropsをリストアップする
TodoListコンポーネントが受け取るpropsはtodosになります

todos = [{
  id: 1,
   completed: true,
   text: 'Todo Item 1',
    }, {
      id: 2,
   completed: false,
   text: 'Todo Item 2',
  }
]

2.各propsが取り得る値をリストアップする

  • 初期状態: todos=[ ]
  • 正常値: todos = [{id:1,...}, {id:2,...}, {id:3,...}]
  • エラー値: なし(配列以外の値がpropsで渡される可能性はありますが、型チェックによってテストできているものとして、今回のテストからは除外します)

3.各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  • 初期状態: Todoリスト(list要素)が描画されない
  • 正常値: todosの配列の要素数分だけ、Todoリスト(list要素)が描画される

4.それぞれの状態をテストケースとする
1〜3をまとめると以下のようになります。

Left align Right align Center align
This This This
column column column
will will will
be be be
left right center
aligned aligned aligned
(a)初期状態のケース (b)正常値のケース
props
 todos [ ] todos=[{id:1,...}, {id:2,...}, {id:3,...}]
期待値
 Todoリストの数 なし 3つ

実際のコードは以下のようになります。

import React from 'react';
import { render } from '@testing-library/react';

import TodoList from './TodoList';

describe('TodoList component', () => {
  describe('(a)初期状態のケース', () => {
    it('has no todo item', () => {
      const todos =[];
      const toggleTodoMock = jest.fn();
      const { queryByTestId } = render(<TodoList todos={todos} toggleTodo={toggleTodoMock}/>);

      // Todoリストの数が0個であることを確認する
      expect(queryByTestId('todo')).toBeNull();
    })
  });

  describe('(b)正常値のケース', () => {
    it('has 3 todo item', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];
      const toggleTodoMock = jest.fn();

      const { queryAllByTestId } = render(<TodoList todos={todos} toggleTodo={toggleTodoMock}/>);
      // Todoリストの数が3個であることを確認する
      expect(queryAllByTestId('todo').length).toBe(3);
    })
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

ここで、queryByTestIdというものが出てきましたので少し補足します。

ByTestIdというのは、APIリファレンスによると以下のようなものになります。

The ...ByTestId functions in DOM Testing Library use the attribute data-testid

In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible.

つまり、画面に表示されるテキスト(ByText)やラベル(ByLabelText)のようにユーザーから見えるDOM要素を取得するべきなのですが、今回の様なテストケースではそういったものがないため、ByTestIdで代用します。

また、ByTestIdで要素を取得できるようにTodo.jsコンポーネントも以下のように修正します。

import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
    data-testid="todo" // ← この行を追加
  >
    {text}
  </li>
)
...
export default Todo

シナリオテスト

次にシナリオテストについて考えてみます。

テストの方針

シナリオテストとしては、ユーザーが実際に操作する方法でテストを行えばよいかと思います。

Write down a list of instructions for that user to manually test that code to make sure it's not broken. (render the form with some fake data in the cart, click the checkout button, ensure the mocked /checkout API was called with the right data, respond with a fake successful response, make sure the success message is displayed).

Turn that list of instructions into an automated test.

テスト設計の手順

具体的なテストケースとしては以下のような手順で設計します。

  1. ユーザーがアプリケーション使用する際のシナリオを書き出す
    要件定義書や設計書があるならば、ユースケース一覧やユースケース記述からシナリオを書き出せばよいかと思います。

  2. 各ユースケースの初期状態を決める

  3. 各ユースケースのイベントで発生する内容を洗い出す

  4. 2と3の期待値を定義する

  5. テスト内容としてまとめる

テスト設計とテストコードの実例

引き続き、Reduxのチュートリアルで使用されているTodoリストアプリを使用して実際のテストケースの設計を行ってみます。

1.ユーザーがアプリケーション使用する際のシナリオを書き出す
シナリオとしては以下の3つになります。

  • (a) Todoリストを追加する
  • (b) フィルターを切り替える
  • (c) Todoリストを完了済みにする

2.各ユースケースの初期状態を決める

  • (a) Todoリストを追加する
    • フィルターは'All'、Todoリストは空の状態
  • (b) フィルターを切り替える
    • フィルターは'All'、Todoリストは3つ登録された状態
  • (c) Todoリストを完了済みにする
    • フィルターは'All'、Todoリストは3つ登録された状態

3.各ユースケースのイベントで発生する内容を洗い出す

  • (a) Todoリストを追加する
    • 空の文字列のTodoリストを追加する
    • Todoリストを追加する
  • (b) フィルターを切り替える
    • フィルターを'Active'に切り替える
    • フィルターを'Completed'に切り替える
  • (c) Todoリストを完了済みにする
    • Todoリストを未完了から完了済みにする
    • Todoリストを完了済みから未完了にする

4.2と3の期待値を定義する

(a) Todoリストを追加する (b) フィルターを切り替える (c) Todoリストを完了済みにする
期待値
 初期状態 (a-1)
・Todoリストなし
・Allのフィルターが選択された状態
(b-1)
・すべてのTodoコンポーネントが表示された状態
・Allのフィルターが選択された状態
(c-1)
・すべてのTodoコンポーネントが表示された状態
・Allのフィルターが選択された状態
 イベント発生後 (a-2)
[空の文字列のTodoリストを追加する]
・Todoリストなし
(b-2)
[フィルターを'Active'に切り替える]
・未完了のTodoリストのみが表示された状態
・Activeのフィルターが選択された状態
(c-2)
[Todoリストを未完了から完了済みにする]
・完了済みにしたTodoリストには取り消し線が引かれる
(a-3)
[Todoリストを追加する]
・Todoリストが1つ
(b-3)
[フィルターを'Completed'に切り替える]
・完了済みのTodoリストのみが表示された状態
・Completedのフィルターが選択された状態
(c-3)
[Todoリストを完了済みから未完了にする]
・未完了にしたTodoリストには取り消し線が削除される

実際のテストコードとしては以下のようになります。

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import reducer from '../reducers'
import App from './App';

function renderWithRedux(
  ui,
  { initialState, store = createStore(reducer, initialState) } = {}
) {
  return {
    ...render(<Provider store={store}>{ui}</Provider>),
    store,
  }
}

describe('App component', () => {
  describe('(a) Todoリストを追加する', () => {
    it('add todo', () => {
      const { getByText, queryByTestId, getByTestId } = renderWithRedux(<App />);

      // (a-1)初期状態の確認
      // Todoリストなし
      expect(queryByTestId('todo')).toBeNull();
      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeDisabled();
      expect(getByText('Active')).toBeEnabled();
      expect(getByText('Completed')).toBeEnabled();

      // (a-2)空の文字列のTodoリストを追加する
      fireEvent.change(getByTestId('input'), { target: { value: '' } })
      // Todoリストなし
      expect(queryByTestId('todo')).toBeNull();

      // (a-3)Todoリストを追加する
      fireEvent.change(getByTestId('input'), { target: { value: '123' } })
      fireEvent.click(getByText('Add Todo'))
      // Todoリストが1つ
      expect(getByText('123')).toBeInTheDocument();

    });
  });


  describe('(b) フィルターを切り替える', () => {
    it('switch filter', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];
      const { getByText, queryByText } = renderWithRedux(<App />, {
        initialState: {todos: todos},
      });

      // (b-1)初期状態の確認
      // すべてのTodoコンポーネントが表示された状態
      expect(getByText(todos[0].text)).toBeInTheDocument()
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[1].text)).toBeInTheDocument()
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[2].text)).toBeInTheDocument()
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (b-2)フィルターを'Active'に切り替える
      fireEvent.click(getByText('Active'))

      // 未完了のTodoリストのみが表示された状態
      expect(getByText(todos[0].text)).toBeInTheDocument()
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[1].text)).toBeInTheDocument()
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')

      expect(queryByText(todos[2].text)).toBeNull()

      // Activeのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (b-3)フィルターを'Completed'に切り替える
      fireEvent.click(getByText('Completed'))

      // 完了済みのTodoリストのみが表示された状態
      expect(queryByText(todos[0].text)).toBeNull()

      expect(queryByText(todos[1].text)).toBeNull()

      expect(getByText(todos[2].text)).toBeInTheDocument()
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Completedのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeEnabled();
      expect(getByText('Completed')).toBeDisabled();

    })
  });

  describe('Toggle todo scenario', () => {
    it('toggle todo', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];

      const { getByText } = renderWithRedux(<App />, {
        initialState: {todos: todos},
      });

      // (c-1)初期状態の確認

      // すべてのTodoコンポーネントが表示された状態
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (c-2)Todoリストを未完了から完了済みにする
      fireEvent.click(getByText(todos[0].text));

      // 完了済みにしたTodoリストには取り消し線が引かれる
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: line-through;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')


      // (c-3)Todoリストを完了済みから未完了にする
      fireEvent.click(getByText(todos[2].text));

      // 未完了にしたTodoリストには取り消し線が削除される
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: line-through;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: none;')

    });
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

またReduxコンポーネントをテストをするときにRedux storeの値をコンポーネントを渡す方法は、公式サイトに記述されている方法を参考にしています。

参考にしたサイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQuery愛好家のためのVue.js、React入門(いずれAngularも)

まず、ことわっておきますが、jQueryは非常に優秀なライブラリです。自分がメインとするWEBシステムの世界ではかなり重宝していますので、時代の潮流だからといって理由もなくjQueryを切り捨てろとは一言も言いませんし、もっと技術は培養すべきです。

ですが、使用できる選択肢を増やす、武器を増やすためにVue.js、Reactを学習するのは非常に有効です。ただ、これらの活用は学習コストが高いといわれています。その原因はフロントエンドありきで話が進みすぎているからだと考えています。したがって、自分の投稿記事は、jQueryを多用するWEBシステムエンジニアに向けた、フォーム操作をメインに置いた半備忘録兼自分なりに解釈した解説です。

ちなみに自分はサーバ構築からバックエンドまでこなしているワンオペエンジニア(フリーランス、非正規雇用に非ず)です。

§1:Vue.jsとReact、そしてAngular

その前に、vue.jsとReactとはどういったもので、どんな意図で開発されたものかを知っておく必要があります。そして、その理念を知っていたら、スクリプト記法の理屈もわかりやすいからです。

Vue.js

Vue.jsはもともとGoogleが開発したAngularJSの開発者の一人が、個人で開発を始めたJSフレームワークです。そのため、小規模の開発に向いた柔軟な利用が可能です(デフォルトでjQueryとの混用も可能)。そのVue.jsとは一言でどんな技術かというと

html内の部品に対して、Vueディレクティブという魔法をかける

こういうものです。ディレクティブとは双方向という意味で、データのインプットとアウトプットをリアルタイムで行うことを意味しています。これをVue.jsやAngularJSなどでは双方向バインディングと呼んでおり、今のAngularでもその技術はある程度継承されています。そして、AngularJSはhtml側でngというプロパティやメソッドを使ってバックエンドの処理をしており、かなり明解だった反面、開発が進むとかなりhtmlソースが汚されてしまうので、技術をある程度継承し、Angularというパッケージ単位のフレームワークを作ったわけです。しかし、このAngularJSの双方向バインディングと簡潔さを捨てるのはもったいないと、飛躍的に発展させたのがVue.jsでありVueディレクティブというもので、これはソースが汚れる原因となったhtml側でのバックエンド処理を、スクリプト側(コンポーネントを作成して管理)で処理させるようにしました。つまり

  • AngularJS: ディレクティブ処理をhtml側で処理できたために、ソースが汚れてしまい、敬遠されてしまった。
  • Vue.js  : ディレクティブ処理をスクリプト側に記載させるようにし、html側は基本、メソッドとプロパティだけ入出力させるようにした(つまり、JavaSrciptの基本に返った)

この理念を覚えておけば、今後の学習にも役立ちます。

React

ReactはFacebookが開発したコンポーネント型のJSフレームワークで、その鍵となるのはJSXという記法です。そのJSXとはなにかというと、スクリプトの中に直接htmlタグを記述できてしまうものです。ですが、その独特の記法によって技術者や入門者に違和感を与え敬遠されてしまっていたのも事実で、そのために何度も記法が変更されてきており、今日ではだいぶ見やすく、そして記述しやすくなっています。それでもReactの基本的な理念と動きは変わっておらず、一言でいうと

html内に魔法結社(JSXの拠点)を作り、そこでディレクティブな部品(htmlタグ)を錬金する

こういうものです。つまり、Vue.jsだとディレクティブとなる部品自体はhtml上にあったのに対し、Reactの場合は、ディレクティブな部品はhtmlになく、バックエンド上のコンポーネントで作成されることになります。そして、ディレクティブな処理を行う際もそのバックエンド内だけで処理するので、非常に動きが高速で、部品もバックエンドにしか存在しないので、開発を分担しやすく中規模の開発に向いています。

  • React :部品の管理を徹底するために、ディレクティブ処理をhtml側で一切できないようにしている。部品の調達と管理はすべてJSXに則ったコンポーネント内で行う。

これが基本です。あと、コンポーネントの記法もいろいろあって、しかもしょっちゅう変更されているので、それが却って敬遠させている(初心者がどこから手を付けたらいいのかわからない)気もしますが、基本となる部分は全くぶれていないことを踏まえておいてください。

Angular(参考)

AngularはAngularJSでの失敗を教訓に、その失敗の原因となったソースの汚れを解消させたものです。そしてvue.js、Reactと違い、RubyのRailsやPHPのLaravel、PythonのDjangoのような、それ一つでパッケージとなっているフルスタックフレームワークとなっており、TypeScriptがベースです。そして、これも一言で表すと

プロジェクト自体がAngularという魔法世界(フレームワーク)である

なので、そのパッケージ内では自由自在にAngularの技術を利用できます。ですが、前述したようにソースの汚れを反省して作ったフレームワークなので、ディレクティブな部品は各アーキテクチャ内で実行するようになっています。ただ、基本はAngularJS時代とそこまで変わっていないためにあまり難しくなく、どちらかというとVue.jsに近いですが、その正直Vue.jsより理屈は解りやすいので、フレームワーク開発に慣れているバックエンドエンジニアなら、上記2つより学習は楽かもしれないです。ただ、けっこう容量があるので、大規模開発向きです(これを省力化、小規模化したJSフレームワークも存在します)。

  • Augular:ディレクティブ処理を行う部品はhtml上にあるが、処理は外部のアーキテクチャ内で行う。

なお、現在もAngularJSはマイナーチェンジを続けていますが、本来のAngularJSの目的はVue.jsが担っていると考えていますので、本記事でAngularJSは採り上げません。

§2:バックエンドのための基本文法

前述したように、これらのJSフレームワークの学習コストが肥大してしまった原因は、フロントエンドありきで解説していることが多いためでしょう。そのため、バックエンドエンジニアが基礎の基礎である文法もわからずに、そっちばかりに目が行ってしまっていることで混乱を招き、これらの技術に手を出す気力を与えず、比較的記述が簡単なjQuery依存から脱皮できないと考えています。

したがって、自分はjQueryで操作してきたことを、Vue.js、Reactではどう記述するのかを重点において説明したいと思います。

演習1:フォーム操作:テキスト文字を表示させる

フォーム操作の基本の基本です。ですが、この基本だけでかなり記法の根本が理解できるのも事実です。ただ、単純に表示させるだけなのも面白くないので、jQueryでフォームに対し、キーの打鍵ごとに値を表示させるようにしましょう。

html
<body>
<input type="text" id="f_inp">
<p>入力された文字<span id="mes"></span></p>
<script>
$(function(){
    $('#f_inp').on("keyup",function(){
        let mes = $(this).val(); //値を取得する
        $("#mes").text(mes); //打ち込んだ値を反映させる
    })
})
</script>
</body>

これで、打ち込んだ文字をそのまま下の#mesに表示させることができます。これとほぼ同じ挙動をVue.js、Reactで再現してみます。

Vue.jsで再現

Vue.jsで記述するとこうなります。そしてVueですが、こっちはコンテンツの後に制御部分を記述してください。

vue-lesson.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
    <div id="app">
        <input type="text" v-model="mes">
        <p>入力された文字<span>{{ mes }}</span></p>
    </div>
<!-- 制御部分はコンテンツの後 -->
<script>
 new Vue({
        el: '#app',
        data:{
            mes: '',
        },
 });
</script>
</body>

jQueryより更に簡潔になっているのは一旦置いといて、目の付け所はelとv-modelいうプロパティであり、これこそがまさしくVue.jsの双方向バインディング技術になります。そして、それが適用される部品は#appに囲んだ単一のブロック要素のみとなります。これをエレメントと呼び、el(elementの略)プロパティで指定した範囲のみ、ディレクティブになるわけです。

もっと詳しく文法を解説する

Vue.jsの場合は、html部分とスクリプト部分は区別しない方が説明もしやすいです。もう一度さっきのhtmlファイルを確認してみましょう。そして、コメントを付与します。

vue-lesson.html
<body>
    <div id="app"><!--#app内のブロック要素にVueディレクティブを適用(1) -->
        <input type="text" v-model="mes"><!-- v:modelはフォームの値を監視するVueディレクティブ(2) -->
        <p>入力された文字<span>{{ mes }}</span></p><!-- {{hoge}}はマスタッシュ -->
    </div>
<script>
 new Vue({
        el: '#app', //適用対象となる要素名(1)
        //dataはVue.jsの操作に必要な変数を格納するオブジェクト
        data:{
            mes: '', //今回は変数mesを使うので定義しておく(中は空白)。(2)
        },
 });
</script>
</body>

このようになります。まずは、部品をディレクティブにするために(1)のように、どこまで適用するのか、その場所を定義し、そこから具体的な動作(2)が行われます。v-modelプロパティはフォーム部品の動きを監視する働きを持っているので、inputの動きに変化(新たに文字が入力された)があると即座に反応し、そしてその結果を{{ mes }}に返すようになっています。

補足1:dataプロパティ

dataプロパティはVueディレクティブで作業するための変数置き場で、これを定義しておかないと、hoge is not definedと処理中に未定義のエラーが起きます。今回はmesという変数を使用しているので、これを定義しておき、そして初期値を空っぽにしておきます。

補足2:マスタッシュ

マスタッシュとは英語で口髭のことで、{{ }}という記号です。そしてVue.jsではVueディレクティブの外側(v-modelなどv-xxxxというプロパティ)に値を適用する場合は{{変数名}}とすることで、その値を受けとることができます。ここでは{{mes}}はVueディレクティブの外側なのでマスタッシュで記述しています(エレメントの外側ではないので注意。エレメントの外側にマスタッシュを記述しても、そこはVue.jsの適用外なので、ただの文字列として認識されるだけです)。

Reactで再現

では、全く同じ動作をReactでも再現してみます。ただ、Reactは少し説明をわかりやすくするために、敢えて最短の記述にしていません(もっとスリム化した記法も使えるのですが、今日、一番見ることが多い記法です)。

react-lesson1.html
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component{
    constructor(){
        super();
        this.state = {
            mes: ''
        };
        this.changeText = this.changeText.bind(this);
    }
    changeText(e){
        this.setState({ mes :e.target.value });
    }
    render(){
        return(
            <div>
                <input type="text" onChange={this.changeText} />
                <p>入力された文字<span>{ this.state.mes }</span></p>
            </div>
        );
    }
}
ReactDOM.render(
    <App />,
    document.getElementById("root")
);
</script>

</head>
<body>
    <div id="root"></div>
</body>

初見の人が見たら、「なんだこりゃ?」となること請け合いです。スクリプトの中にhtmlタグが入っているのが生理的に受け付けないのでしょう。しかし、Reactも初期と比較すると、だいぶ記述はスリム化しており、そして解りやすくなっています。では、この処理の流れがわかるようにコメントを付与してみます。なお、Vue.jsでは、スクリプトをbodyタグの内側、コンテンツの後に書くのがセオリーでしたが、Reactはコンテンツの前、headタグの中に記述するのがセオリーです(普通は外部ファイル化します)。

react-lesson1.html
<script type="text/babel">
//Reactコンポネントを作成するクラス(1)
class App extends React.Component{
    //A:定義部分。簡単にいえば、処理前に準備する変数やメソッドなど。
    constructor(){
        super();
        this.state = {
            mes: ''
        };
        this.changeText = this.changeText.bind(this); //(3)値をバインドさせる準備
    }
    //B:処理関数部分。renderの外側に書く方が見やすい。
    //入力された文字を返す関数(2)
    changeText(e){
        this.setState({ mes :e.target.value }); //setStateは値をバインドさせる処理(3)
    }

    //C:レンダリング部分。JSX記法のhtmlタグを描画する。
    render(){
        //JSX記法で返す部品(2)
        return(
           //onChangeハンドラ以下はReactでバインディング処理を行う関数(3)
            <div>
                <input type="text" onChange={this.changeText} />
                <p>入力された文字<span>{ this.state.mes }</span></p>
            </div>
        );
    }
}
ReactDOM.render(
    <App />, //コンポネント作成を行う部品(1)。記述方法にルールがある。
    document.getElementById("root") //外側に部品を表示させる(4)
);
</script>

</head>
<body>
    <div id="root"><!-- ここに処理部品が入る(4)--></div>
</body>

だいたいこのような流れになっています。16.8からはもっと簡略化した記法も使えるのですが、一般的に知られている記法をマスターしてからの方がいいでしょう。見た目は複雑に見えますが、まずは次のように分類しましょう。

  • React.Component() //コンポネントを作成します
  • ReactDOM.render() //作成したコンポネントを反映させます

renderとはいろいろな意味がありますが、レンダリングという言葉通り、描写という言葉で覚えればいいでしょう。そして、その名の通り、ReactDOM.render内ではJSX記法に則ったhtmlタグを描写しているのです。そしてその部品を作成するのがReactDOM.Componentであり、いわば、前者がシステムの納入、後者がシステムの開発や保守管理だと思えばいいでしょう。したがって、htmlタグ側にはリアクティブ処理された部品しか存在していなく、スクリプトの処理部分は、Vue.jsと違って部品すら見えません。

では、今度はReact.Componentの中身を3つに分類してみます。

  • constructer() //定義部分
  • 処理用の関数 //処理部分
  • render() //レンダリング部分

だいたいはこのように分類するとわかりやすいです。そして、その作業の流れを文章化してみると

  1. Appという部品を作成する命令を出す(1)
  2. 命令を受けたコンポネントはコンストラクタから部品を用意してレンダリング領域にある通り、タグを作成し、外側に返す(2)(4)
  3. onChangeイベントが発火したら、用意していた関数を使って処理を行う(3)
  4. 処理を行った状態で再度レンダリングを行い、ReactDOM.renderを使用して処理の外側に返す(4)

それを踏まえた上で処理を追ってみるとやっていることはJavaScriptとそこまで相違ないと気づくはずです。

ただ、決定的な違いとして処理関数内にreturnが存在しない(レンダリング部分ではない)、そしてreturn処理を行う代わりにsetStateで制御することです。そして、これによってバインディングが可能になります。

オブジェクト、変数の利用

Reactでもう一つ躓きがちなのが、オブジェクトや変数の利用に際しては、同じコンポーネント内であっても、外部の存在として扱わないといけないということです。具体的に言うと、thisという代名詞を置いているのがそれで、それぞれ、定義部分、レンダリング部分、関数部分で変数や関数をやりとりする際には、thisという代名詞を使っているのがわかると思います。

  • 関数部分のchangeTextをレンダリング部分で使用する場合、this.changeTextとして呼び出し
  • レンダリング部分でmesを使用するために、定義部分のstate.mesをthis.state.mesとして呼び出し

このような具合です。またsetStateのようにコンポーネントで用意された部品を使用する際も、外部から借用することになるので、this.setStateと代名詞を付与しています。

補足:バインディングのルール

定義部分で

    this.changeText = this.changeText.bind(this);

とあると思いますが、これは別におまじないではなく、利用する関数において値をバインド(同期)したいときに記述するルールで、これを記述しないと値をバインドできません。そして、基本は同じオブジェクト名にしておくだけで、左側は変数を代入しているだけですので、定義さえしておけば、別名でも大丈夫です(したところでメリットが薄いので同名にしおくべきですが)。

また、setStateはReactにおいて極めて重要性の高いメソッドですが、簡単にいえば、元の値と新しい値をバインドさせたいときに設定するものです。具体的には先程入力した文字と新たに入力した文字が異なる場合、随時setStateメソッドが実行されることになります。

また、e.target.valueはJSX内にある部品の、任意のフォームに対して取得した値(value)を受けとるもので、e(jQueryではelemと書くことが多い)とは、任意のオブジェクト変数に過ぎません。そして、受け取った値を先程のsetStateメソッドを使って、値の変化を処理しているわけです。

最新の書き方(16.8以降)

上記の方法でもかなり簡潔になりました(ECMA5時代はコンポネントも逐一作成する必要がありました)が、それでも至るところにthisばかりあったりと、色々と無駄があるようにも見えました。それをスリム化させるとこのようになります。

react-renew.html
<script type="text/babel">
    //定義部分
    const { useState } = React; //ローカルの場合で、useStateを使用するための定義
    const App =()=>{
        //処理部分
        const [mes,setMes] = useState(true); //バインディング処理
        //一度メソッドで準備する
        const changeText = (e)=>{
            setMes( e.target.value ); //値の比較
        }
        //レンダー部分
        return(
        <div>
            <input onChange={changeText} />
            <p>入力された文字<span>{ mes }</span></p>
        </div>
            );
    }
        const elem = <App />; //ワンクッション置かないと警告が出る
    //レンダリング処理
    ReactDOM.render(
        elem,
        document.getElementById("root")
    );
</script>

代名詞のthisが消えてかなり明白になったと思いますが、基本的な動作は変わっていません。ただ、変数定義部分、処理部分、レンダリング部分が一つの関数に収まったので、すごくすっきりします。

ただ、注意することとして

html
    const App =()=>{....}
    const.elem = <App />; //ワンクッション置く
    ReactDOM.render(
        elem,
        document.getElementById("root")
    );
    //ダイレクトに呼び出す以下の書き方は推奨されていない(警告が出ます)
    const App =()=>{....}
    ReactDOM.render(
        <App />,
        document.getElementById("root")
    );

この部分で、このようにrenderメソッドに対して、ワンクッション置かないと警告メッセージが表示されることになります。

演習2:プルダウンメニューの制御

以下作成中

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む