Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
8
Help us understand the problem. What is going on with this article?
@susiyaki

React Redux Basic Tutorial 俺なりのまとめ

Reduxとは

facebook社が提案したfluxを元に、javascriptでアプリのステート管理を行うためのツール。
各コンポーネントで持っていたstateや関数をstorereducerと呼ばれる場所で持つことで、全てのコンポーネントからアクセス可能となる。

必要性

大規模なアプリケーションになり、stateが多く存在するときなどに導入するといいらしいです。
stateが多くなればなるほど、バケツリレーが大量に発生し、エラーが発生する可能性が大きくなってしまう為です。
このへんは検索すればたくさん出てくるので、reduxのフローなども含めて調べてみてください。

本記事の目的

僕自身アルバイトでこれからReactを書ける環境に行けることや、製作中の個人ブログのフロント部分をReactで書くために学びたいと思っていたのでメモしたいと思い、書きました。

初めは個人ブログのフロントをfluxも使わずに書いていたのですが、同じようなstateが複数出てきたときに管理が難しくなったため、fluxreduxを用いることに決めました。

reduxに決めた要因としては、facebook社が提供するfluxが2年前くらいから更新が止まっているとの情報から、学習コストは多少高くてもreduxを覚えてしまおうというところです。

では、見ていきましょう。

導入

完成図

スクリーンショット 2019-07-05 7.39.40.png

Basic Tutorial: Intro · Redux

環境

環境はこのような感じです。

macOS mojave
node v12.6.0
redux ^4.0.1
yarn 1.17.0

本記事ではyarnを用います。npmの方は適宜読み替えてください。
yarnbrewでインストールすることが推奨されているようです。

create-react-app

まずnpxを用いてプロジェクトを作成します。

npx create-react-app todo-tutorial-redux

次に、一旦作成されたプロジェクトの起動確認をしていきます。

cd todo-tutorial-redux
yarn start

ロゴが表示されたらokです。

次に、、必要なツールを入れていきます。
yarn add redux react-redux

これで準備は完了です。

Container ComponentsとPresentational Components

いきなりですが、公式tutorialの順番とは大きく入れ替わって、まずはContainer ComponentsPresentational Componentsについて見ていきます。
reduxではコンポーネントの種類を大きくこの2つに分けており、それぞれのルールに則って作ることで、再利用性の高いコードが書けるようになります。

ComponentとContainerについて - Qiita

こちらを参考にさせていただきました

Presentational Component

このコンポーネントは見た目を担当するコンポーネントで、reactで普段用いるコンポーネントと変わりありません。

  • stateを基本持たない(持つ場合はデータではなく、UIの状態)
  • Actions, Store(reduxの他の要素)に依存しない
  • 基本的にfunctional componentsとして書かれる

Container Components

reduxconnectという機能を用いることで、ロジックに関与することができるコンポーネントになります。
通常、このコンポーネントはスタイルを持たず、下位のコンポーネントのデータソースとして機能します。

mapStateToPropsmapDispatchToPropsというところがキモで、

  • mapStateToProps : storeで管理するstatepropsとして扱うことができる。
  • mapDispatchToProps : Actionsで定義した関数をpropsとして扱うことができる。

これを使って、redux管理をしているものをPresentationl Componentsで表示したり、関数を実行したりできます。

ディレクトリ構造

公式tutorialでは、Presentational ComponentsComponentsディレクトリに、Container ComponentsContainersディレクトリに分けています。
しかし、これらを分けることは必須ではなく、まとめて記述しても問題ありません。

Actions

ここからはtutorial通りに進めていきます。Actionsstoreに対しての動作を書くところです。
何かが起きたときに、起きたことによって何をするか、そのときに用いるデータと共に定義しておき、Reducersへ報告します。(Actionsの時点ではstoreの状態は変化しません。)
そして、これを実際に呼び出すところがContainer Componentsです。

Actionsは、action typesというユーザが行った動作を文字列で指定したものを持ちます。この文字列は次に説明するReducersと対応します。
役割としては、Actionsで発火、Reducersで処理をする際の命令を投げる先の指定といったところです。

export const ADD_TODO = 'ADD_TODO'

そして、action creatorsaction typesで定義したデータとそのアクションに必要なデータをまとめます。Reducersはここから呼び出されます。

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

単純な関数として記述されていることがわかります。ユーザがTODOを追加したとき、そのaction typesとその動作に必要なデータとしてtextを返しています。

actions.js

actions.js
/*
 * action types
 */

export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * other constants
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action creators
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Reducers

Reducersはアクションから何が起きたかをしらされたときに実際に行なう処理を書きます。

その処理は、以下のように
(previousState, action) => newState
過去のstateと処理の内容から新しいものを生み出すというものです。

Reducersの注意点として、

  • 引数が不自然に変更されてはいけない(同じ引数を渡したら常に同じ結果を返す)
  • 非同期処理など副作用を生み出すものを書けない(redux-thunkredux-sagaといったMiddleWareを使わないといけない)
  • Dateや乱数などのnon-pure functionは使用できない

という点があります。

Reducersの返す値

Reducersはアクションを受け取ってなにか処理を行い、反映させます。
しかし、初回の表示の際に表示するデータが定義されていない場合、うまく動かないことがあるため、アクションがない場合にはinitialStateを定義しておいてそれを返すようにします。

function todoApp(state = initialState, action) {
  // For now, don't handle any actions
  // and just return the state given to us.
  return state
}

重要なのはstate = initialStatereturn stateです。

実際に処理のあるパターンを見てみましょう。

import {
  ADD_TODO
} from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

action typesをインポートして、それを元にswitchで処理を選択しています。
先程書いた初期データを返す命令はdefaultで定義されます。

  1. このreducersADD_TODOのアクションが呼ばれたら
  2. stateを今のstate(...state.todos)と送られてきたtextを追加したものをとして更新して返す

といった処理をしています。

Reducersの分割

処理を書いていくうちに、規模が大きくなったり機能が入り混じったりして、見通しが悪くなってしまうことが出てくると思います。

以下の例では、TODO一覧をフィルター表示するためのものとTODOを追加・トグルするものというもので分けたほうが見やすそうです。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

reduxreducerを1つしか返すことができません。しかし、複数のreducerをまとめ上げたものを返すことは可能です。そのための機能として、combineReducersというものがあります。

以下のように、todosvisibilityFilterというreducerに分ける場合、

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

このようにまとめることができます。

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

reducers.js

reducers.js
import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Store

Storeはアプリケーションにつき1つしか存在しません。
役割として、

  • getState()stateへアクセスできる
  • dispatch(action)stateを更新できる
  • subscribe(listener)でリスナーを登録できる
  • subscribeで登録した関数でunsubscribeできる
import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// Log the initial state
console.log(store.getState())

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() => console.log(store.getState()))

// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// Stop listening to state updates
unsubscribe()

zMMtoMz.png
スクリーンショット 2019-07-05 7.39.40.png

store.js

combineでReducersでまとめたreducersを元にstoreを作成します。

store.js
import { createStore } from 'redux'
import todoApp from './reducers'

const store = createStore(todoApp)

Component

以下の設計でTODOListを作成します。

Presentational Components

  • TodoList : TODOのリスト表示
    • todos : {id, text, completed}という構成の配列
    • onTodoClick(id: number) : 未完了・完了をtoggleする
  • Todo : TODO単体
    • text: string : 本文
    • completed: boolean : TODOの状態
    • onClick() : callback
  • Link : callback
    • onClick()
  • Footer : 表示フィルターの選択
  • App : root component

Container Components

  • VisibleTodoList : TODOのフィルター後のデータを保持する
  • FilterLink : 現在Linkで選択されているフィルターからフィルターを行う
    • filter: string

Other Components

  • AddTodo : Addボタン

components/Todo.js

components/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'
    }}
  >
    {text}
  </li>
)

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

単純にTodoを表示し、onTodoClick(index)を実行を渡します。

components/TodoList.js

components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map((todo, index) => (
      <Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
    ))}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  onTodoClick: PropTypes.func.isRequired
}

export default TodoList

todosを全て表示します。

container/VisibleTodoList.js

container/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

TodoListで表示に用いているtodosをフィルターしています。
filterの値によって、getVisibleTodostodosの内容が変わっていることがわかると思います。

mapStateToPropsgetVisibleTodosに現在のtodosとフィルターの種類を渡すことで、新しいtodosを定義しています。

mapDispatchToPropsTodoコンポーネントがクリックされたときに発火し、toggleTododispatchします。

また、connectを用いて、TodoListコンポーネントで定義した関数を使えるようにしています。

components/Footer.js

components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'

const Footer = () => (
  <p>
    Show: <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
    {', '}
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
    {', '}
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
  </p>
)

export default Footer

フィルターのaction typesFilterLinkへ渡しています。

components/Link.js

components/Link.js
import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    <a
      href=""
      onClick={e => {
        e.preventDefault()
        onClick()
      }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link

選択されている状態の場合はただのspanで、選択されていない場合はonClickを実行するaタグを出力する。

containers/FilterLink.js

containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

まず、ownProps.filterFooterで選択されたフィルターを持ちます。
Linkがクリックされると、setVisibilityFilterdispatchされます。
そして、押されたリンクはactiveを持ち、そのリンクだけがspanで表示されるようになります。

参考:reducer部分

reducer.js
...

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

...

containers/AddTodo.js

containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
  let input

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault()
          if (!input.value.trim()) {
            return
          }
          dispatch(addTodo(input.value))
          input.value = ''
        }}
      >
        <input
          ref={node => {
            input = node
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo

このコンポーネントはContainer Componentsですが、スタイルも入っている例です。
AddTodoinputの値で変わるため、letで宣言されています。
送信されたら、入力内容をaddTododispatchしています。

components/App.js

components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

export default App

index.js

index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

const store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

indexでは、Providerを使ってstoreを使えるようにする必要があります。

まとめ

読んでみると少し理解が深まった気がしますが、非同期通信を用いた方法や、複雑な処理を行う場合、自分で書くためにはもう少し勉強しなきゃなと感じました。

次は、非同期通信を用いたパターンについてまとめる予定です。
ご指摘、アドバイスがあれば宜しくおねがいします。

8
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
susiyaki
つよつよエンジニアになりたい
planningdev
九州工業大学 ITサービス開発・運用団体

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
8
Help us understand the problem. What is going on with this article?