LoginSignup
12
13

More than 3 years have passed since last update.

Railsの人がCRUDを作りReduxを学ぶ。

Last updated at Posted at 2019-05-07

はじめに。 なぜReduxはわかりにくいのか?

A. 以前の経験の類推が効かない新しい概念だから。

*注意:個人の主観です。

では、どうすればわかりやすくなるのか?

A. 以前の経験と結びつけてあげる。
- Rails経験者はCRUDアプリの作成経験ありますよね?

この記事のゴール

★Reduxを使いシンプルなPostモデルのみのCRUDアプリを作ること.

対象読者

  • react.jsを使ってCRUDアプリを作った経験はある
  • railsでCRUDを作成経験がある

Redux関連の使用技術

  • redux
  • redux-thunk

完成のgithub repo


では、やっていきましょう!!!
内容がよかったら、いいねお願いします!

1. Rails Apiのバックエンドの下準備

*この章のゴールは、railsのバックエンドをパパッとscaffoldで作ることです。

1-1 rails アプリの作成とscaffold

terminal

rails new redux-crud-api --api
cd redux-crud-api
rails generate scaffold Post title:string content:text
rails db:migrate

app/controllers/posts_controller.rbdef indexのOrderを変更

...
  def index
    @posts = Post.all.order(created_at: :desc)

    render json: @posts
  end
...

1-2 ダミーデータの作成

db/seeds.rb

Post.create(title: "Lorem ipsum dolor sit amet", content: "Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet")
Post.create(title: "consectetur adipiscing elit", content: "Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa")
Post.create(title: "Integer nec odio", content: "Vestibulum lacinia arcu eget nulla.")

terminal

rails db:seed

1-3 rack-corsを追加する

Gemfile

gem 'rack-cors'
をアンコメントアウトする

terminal

bundle

config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

1-4 ちゃんと動くかテスト

terminal

rails server -p 3001

http://localhost:3001/posts.json
にアクセス
スクリーンショット 2019-05-07 1.33.53.png

この章はこれで、終了です。

2. create-react-appでreactアプリを作成

terminal

create-react-app redux-crud-client
cd redux-crud-client
yarn start

*動くことを確認したら、この章はokです!

3. 必要なnpmパッケージをインストールする

terminal

yarn add axios redux redux-thunk

*公式や他のチュートリアルでは、axiosではないが使いやすいため.

4. react-routerを使えるようにするのとCRUDページの準備

*この章のゴールは、clientのCRUDのページを先に用意して、やることのイメージがつきやすい様にすることです!

この章は、reduxとはそんなに関係ないので、ただのコピペで考えないでいいと思います!

4-1 インストール

とりあえず、Reduxの公式ドキュメント通りにやる。

terminal

yarn add react-router-dom

4-2 src/index.js

*index.js
react-routerを使うとき公式ではどうなっているかというと、下図のようになっている...
スクリーンショット 2019-05-07 5.00.20.png
(https://redux.js.org/advanced/usage-with-react-router#indexjs)
またはredux-thunkを使った時のexampleでは下図のようになっている,
スクリーンショット 2019-05-07 7.03.35.png

(https://redux.js.org/advanced/async-actions)

これらを真似して, 以下のようにした(思考停止でいいと思います)
index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import Root from './components/Root'
import rootReducer from './reducers'

const store = createStore(rootReducer, applyMiddleware(thunk))

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

足りないRoot.jsrootReducerを作る必要があるので作っていきましょう.

4-3 src/components/Root.js

Root.jsは公式ドキュメントでは下図の様な感じ(https://redux.js.org/advanced/usage-with-react-router#components-rootjs)
スクリーンショット 2019-05-07 5.13.52.png

これを真似て、

components/Root.jsの中身は以下のようにした。

import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import { Router, Switch, Route } from 'react-router-dom' //*これは公式と違う以下のurl参照. まぁ、先人の失敗と思いスルーしても問題なし。
import history from '../history' //*これは公式と違う https://stackoverflow.com/questions/42701129/how-to-push-to-history-in-react-router-v4
import TopPage from '../containers/TopPage'
import PostNew from '../containers/PostNew'
import PostShow from '../containers/PostShow'
import PostEdit from '../containers/PostEdit'
import Navbar from './Navbar'

// https://stackoverflow.com/questions/50584641/invariant-violation-you-should-not-use-switch-outside-a-router
// https://reacttraining.com/react-router/web/api/Switch
// https://stackoverflow.com/questions/46621334/react-react-router-dom-two-route-conflict
const Root = ({ store }) => (
  <Provider store={store}>
    <Router history={history}>
      <Navbar />
      <Switch>
        <Route exact path="/" component={TopPage} />
        <Route exact path="/posts/new" component={PostNew} />
        <Route exact path="/posts/:id" component={PostShow} />
        <Route exact path="/posts/:id/edit" component={PostEdit} />
      </Switch>
    </Router>
  </Provider>
)

Root.propTypes = {
  store: PropTypes.object.isRequired
}

export default Root

このファイルをみて、だいたいCRUDごとにページ分けしているんだ!と感じられたらOKです!
*historyとか、react-routerのルーティングとかは主題じゃないので。


では、ここからコツコツ足りないファイルを追加していきましょう。
以下は、特にReduxとは関係ないので、思考停止コピペマシーンになってください。
*
まだ作成されていないcontainersフォルダーは作成してください!**

1-components/Navbar.js

import React from 'react';
import { NavLink } from 'react-router-dom'

const ulStyle = {
  display: 'flex',
  listStyleType: 'none',
  padding: '20px 30px',
  background: '#eee',
  margin: 0
}

const Navbar = () => (
  <nav>
    <ul style={ulStyle}>
      <li style={{ flex: '1 0 auto'}}>
        <NavLink
          exact
          activeStyle={{
            fontWeight: "bold",
            color: "red"
          }}
          to="/"
        >
          Top
        </NavLink>
      </li>
      <li>
        <NavLink
          exact
          activeStyle={{
            fontWeight: "bold",
            color: "red"
          }}
          to="/posts/new"
        >
          Add New Post
        </NavLink>
      </li>
    </ul>
  </nav>
)
export default Navbar

2-containers/TopPage.js

import React, { Component } from 'react';

class TopPage extends Component {
  render() {
    return (
      <h1>TopPageです</h1>
    )
  }
}

export default TopPage

3-containers/PostNew.js

import React, { Component } from 'react';

class PostNew extends Component {
  render() {
    return (
      <h1>PostNewです</h1>
    )
  }
}

export default PostNew

4-containers/PostNew.js

import React, { Component } from 'react';

class PostNew extends Component {
  render() {
    return (
      <h1>PostNewです</h1>
    )
  }
}

export default PostNew

5-containers/PostEdit.js

import React, { Component } from 'react';

class PostEdit extends Component {
  render() {
    return (
      <h1>PostEditです</h1>
    )
  }
}

export default PostEdit

6-containers/PostShow.js

import React, { Component } from 'react';

class PostShow extends Component {
  render() {
    return (
      <h1>PostShowです</h1>
    )
  }
}

export default PostShow

7-src/history.js

import { createBrowserHistory } from 'history';

export default createBrowserHistory();

4-4 reducers/index.js を作成

公式ドキュメントでは下図の様になっている(https://redux.js.org/basics/reducers)...
スクリーンショット 2019-05-07 5.52.38.png
これを真似て...
src/reducers/index.js

import { combineReducers } from 'redux'

export default combineReducers({
/* あとで追加 */
})

4-5 動作確認!

動くか確認してください!!
Routingがきっちりできていれば大丈夫です!


*では、次の章からは、本題のReduxの使い方になっていきます! 気合いいれていきましょう!

*基本的には、ファイルをコピペしてもらって、

1. 流れを掴む
2. コメントを読んで、少し理解する
3. 納得いかない箇所を自分で調べる

という感じで進めていってもらえると助かります!

5 トップページでPostsリストを表示させる

*この章のゴールのイメージ
スクリーンショット 2019-05-07 7.05.28.png

5-1 src/containers/TopPage.js

src/containers/TopPage.js
なにをやりたいか?という着眼点で,コメントしてます。

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getPosts } from '../actions' /* これを定義する必要がある */

class TopPage extends Component {
  componentDidMount () {
    // 初めは空のstoreにあるposts state
    // getPostsをdispatchすることで
    // backendのrails apiをコール
    // 帰ってきたデータをstoreのposts stateに注入
    // このコンポーネントはstoreに繋がっているので、それが反映される

    // https://react-redux.js.org/using-react-redux/connect-mapdispatch
    // 上にある通り、connectでTopPageが囲まれるとdispatch propsが渡される
    const { dispatch } = this.props
    dispatch(getPosts())
  }

  render() {
    const { posts } = this.props

    if(posts.length) {
      return (
        <div>
          <h4>posts</h4>
          {posts.map(post => {
            return (
              <div key={ post.id }>       
                <h4>
                  <Link to={`/posts/${post.id}`}>
                    {post.id}: {post.title}
                  </Link>
                </h4>
                <p>{post.content}</p>
              </div>
            )
          })}
        </div>
      )    
    } else {
      return (<div>No posts</div>)
    }
  }
}

/* Storeのposts stateをTopPageコンポーネントのposts propsに渡したい */
const mapStateToProps = (state) => {
  const { posts } = state
  return { posts }
}

/* ReduxのStoreと、このTopPageコンポーネントに接続する */
export default connect(mapStateToProps)(TopPage)

5-2 actions/index.js

対応する公式ドキュメント(https://redux.js.org/advanced/async-actions#async-action-creators)

actinos/index.js

import axios from 'axios'

const apiUrl = 'http://localhost:3001'

/* 理解するより慣れた方が早い気がします... */
// Actions are payloads of information that send data from your application to your store.
// They are the only source of information for the store.
// You send them to the store using store.dispatch().

// action types
export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'

// action creators
const fetchPostsSuccess = posts => ({
  type: FETCH_POSTS_SUCCESS,
  posts
})

// async action creator
export const getPosts = () => {
  return (dispatch) => {
    return axios.get(`${apiUrl}/posts`)
      .then((response) => {
        dispatch(fetchPostsSuccess(response.data))
      })
      .catch((error) => {
        console.log(error)
      })
  }
}

*注意: 公式ドキュメントでは以下の様に3つのaction typeを分けています。
これについては、あとで触れます!
スクリーンショット 2019-05-07 8.01.56.png
https://redux.js.org/advanced/async-actions#actions

5-3 reduces/index.jsreducers/postsReducers.js

reducers/index.js
公式ドキュメントでは...
スクリーンショット 2019-05-07 8.11.50.png

では、これらを参考に書いていきましょう。

reducers/index.js

import { combineReducers } from 'redux'
import {
  posts
} from './postsReducer'

/* 公式ドキュメントのreducersの定義 */
// Reducers specify how the application's state changes in response to actions sent to the store.
// Remember that actions only describe what happened, but don't describe how the application's state changes.

/* これが、src/index.js でstoreと繋がってるのを忘れずに! */
/* storeが意識しずらいのが理解し難い原因かも... */
/* storeをそれ自身ではなく、変化するもの(reducers)で外堀から定義していく */
export default combineReducers({
  posts
})

reducers/postsReducers.js

import {
  FETCH_POSTS_SUCCESS,
} from '../actions'

export function posts(state = [], action) {
  switch (action.type) {
      case FETCH_POSTS_SUCCESS: //このactionがdispatchされたら、action.postsを返すよ~
        return action.posts
      default: //なにもされてないときは、デフォルトの[]を返すよ~
        return state;
  }
}

公式では、todosだけでファイルを作成している。 postsReducersでファイルを分けたのは、あとで説明します。これは好み? Reduxは好みが色々あるからわかりにくいですね...。
https://redux.js.org/basics/example#reducers-todosjs
スクリーンショット 2019-05-07 8.14.27.png

5-4 動作確認

ここで、一度立ち止まって確認してみてください!

6 エラーハンドリングをする

この章でやること
以下の様なエラーハンドリングをする。
https://redux.js.org/advanced/async-actions#actions
スクリーンショット 2019-05-07 19.45.24.png

6-1 react.jsの時の対応関係を振り返る

import React, { Component } from 'react'

class TopPage extends Component {
    this.state = {
        hasError: false,
        isFetching: false,
        posts: []
    }
  componentDidMount () {
        this.setState({
            isFetching: true
        })
        axios.get('/posts')
            .then((res) => {
                this.setState({
                    posts: res.data,
                    isFetching: false
                })
            })
            .catch((error) => {
                this.setState({
                    isFetching: false,
                    hasError: true
                })
            })
  }

  render() {
        const { posts, hasError, isFetching } = this.state
        if (hasError) {
            return(
                <p>Something wrong happend...</p>
            )
        }

        if (isFetching) {
            return (
                <p>Fetching, posts...</p>
            )
        }

        if (posts.length) {
            return (
                ...
            )
        }
  }
}

export default TopPage

これをReduxでやりたい。というのが6章のゴールです!

6-2 actions/index.jsを編集

actions/index.js

import axios from 'axios'

const apiUrl = 'http://localhost:3001'

/* まぁ, 理解するより慣れた方が早い気がします. */
// Actions are payloads of information that send data from your application to your store.
// They are the only source of information for the store.
// You send them to the store using store.dispatch().

// action types
// 公式のより、下記のurlの定義の方が分かりやすかった。
// https://github.com/stowball/dummys-guide-to-redux-and-thunk-react/blob/master/src/actions/items.js
export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'
export const IS_FETCHING_POSTS = 'IS_FETCHING_POSTS'
export const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE'

// action creators
const fetchPostsSuccess = posts => ({
  type: FETCH_POSTS_SUCCESS,
  posts
})

const fetchPostsFailure = (bool) => ({
  type: FETCH_POSTS_FAILURE,
  fetchPostsFailure: bool
})

const isFetchingPosts = (bool) => ({
  type: IS_FETCHING_POSTS,
  isFetchingPosts: bool
})

// async action creator
export const getPosts = () => {
  return (dispatch) => {
    dispatch(isFetchingPosts(true))
    return axios.get(`${apiUrl}/posts`)
      .then((response) => {
        dispatch(isFetchingPosts(false))
        dispatch(fetchPostsSuccess(response.data))
      })
      .catch((error) => {
        dispatch(isFetchingPosts(false))
        dispatch(fetchPostsFailure(true))
      })
  }
}

6-3 reducers/postsReducer.jsを編集

*これが、posts.jsではなくpostsReducer.jsでファイルを作成した理由。
reducers/postsReducer.js

import {
  FETCH_POSTS_SUCCESS,
  IS_FETCHING_POSTS,
  FETCH_POSTS_FAILURE
} from '../actions'

export function isFetchingPosts(state = false, action) {
  switch (action.type) {
      case IS_FETCHING_POSTS:
        return action.isFetchingPosts;
      default:
        return state;
  }
}

export function fetchPostsFailure(state = false, action) {
  switch (action.type) {
      case FETCH_POSTS_FAILURE:
        return action.fetchPostsFailure;
      default:
        return state;
  }
}

export function posts(state = [], action) {
  switch (action.type) {
      case FETCH_POSTS_SUCCESS: //このactionがdispatchされたら、action.postsを返すよ~
        return action.posts
      default: //なにもされてないときは、デフォルトの[]を返すよ~
        return state;
  }
}

6-4 reducers/index.jsを編集

reducers/index.js
*これで、fetchPostsFailureisFetchingPostsがconnectのstate=>propsで使えるようになる.

import { combineReducers } from 'redux'
import {
  posts,
  fetchPostsFailure,
  isFetchingPosts
} from './postsReducer'

/* 公式ドキュメントのreducersの定義 */
// Reducers specify how the application's state changes in response to actions sent to the store.
// Remember that actions only describe what happened, but don't describe how the application's state changes.

/* これが、src/index.js でstoreと繋がってるのを忘れずに! */
/* storeが意識しずらいのが理解し難い原因かも... */
/* storeをそれ自身ではなく、変化するもの(reducers)で外堀から定義していく */
export default combineReducers({
  posts,
  fetchPostsFailure,
  isFetchingPosts
})

6-5 containers/TopPage.jsを編集

containers/TopPage.js

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getPosts } from '../actions'

class TopPage extends Component {
  componentDidMount () {
    // 初めは空のstoreにあるposts state
    // getPostsをdispatchすることで
    // backendのrails apiをコール
    // 帰ってきたデータをstoreのposts stateに注入
    // このコンポーネントはstoreに繋がっているので、それが反映される

    // https://react-redux.js.org/using-react-redux/connect-mapdispatch
    // 上にある通り、connectでTopPageが囲まれるとdispatch propsが渡される
    const { dispatch } = this.props
    dispatch(getPosts())
  }

  render() {
    const { posts, isFetchingPosts, fetchPostsFailure } = this.props
    if (isFetchingPosts) {
      return (
        <p>Fetching posts...</p>
      )
    }

    if (fetchPostsFailure) {
      return (
        <p>Failed to fetch posts...</p>
      )
    }

    if(posts.length) {
      return (
        <div>
          <h4>posts</h4>
          {posts.map(post => {
            return (
              <div key={ post.id }>       
                <h4>
                  <Link to={`/posts/${post.id}`}>
                    {post.id}: {post.title}
                  </Link>
                </h4>
                <p>{post.content}</p>
              </div>
            )
          })}
        </div>
      )    
    } else {
      return (<div>No posts</div>)
    }
  }
}

/* Storeのposts stateをTopPageコンポーネントのposts propsに渡したい */
const mapStateToProps = (state) => {
  const { posts, isFetchingPosts, fetchPostsFailure } = state
  return { posts, isFetchingPosts, fetchPostsFailure }
}

/* ReduxのStoreと、このTopPageコンポーネントに接続する */
export default connect(mapStateToProps)(TopPage)

6-6 動作確認

うっする、Fetching posts...が見えるはず...。

7 PostsNewページを完成させる

この章のゴールは
2019-05-07 22.33.49.gif

7-1 containers/PostNew.jsを編集する

containers/PostNew.js

import React from 'react'
import { connect } from 'react-redux'
import { addPost } from '../actions'

class PostNew extends React.Component {
  state = { 
    title: '',
    content: ''
  }

  handleChange = (event) => {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit = (event) => {
    event.preventDefault()
    const { dispatch } = this.props
    dispatch(addPost(this.state))   
  }

  render() {
    const { isAddingPost, addPostFailure } = this.props
    return (
      <div>
        {isAddingPost &&
          <p>Adding post now...</p>
        }
        {addPostFailure &&
          <p>Failed to add post...</p>
        }
        <h4>Add New Post</h4>
        <form onSubmit={ this.handleSubmit }>
          <div>
            <input
              type="text"
              name="title"
              required
              value={this.state.title}
              onChange={this.handleChange} 
              placeholder="Title"
            />
          </div>
          <div>
            <textarea
              name="content"
              rows="5"
              required
              value={this.state.content}
              onChange={this.handleChange} 
              placeholder="Content"
            />
          </div>
          <button type="submit">Create</button>
        </form>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  const { isAddingPost, addPostFailure } = state
  return { isAddingPost, addPostFailure }
}

export default connect(mapStateToProps)(PostNew)

7-2 actions/index.jsを編集する

actions/index.jsに以下のコードを追加

import history from '../history'
...

export const IS_ADDING_POST = 'IS_ADDING_POST'
export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS'
export const ADD_POST_FAILURE = 'ADD_POST_FAILURE'

const isAddingPost = (bool) => ({
  type: IS_ADDING_POST,
  isAddingPost: bool
})

const addPostSuccess = (post) => ({
  type: ADD_POST_SUCCESS,
  post
})

const addPostFailure = (bool) => ({
  type: ADD_POST_FAILURE,
  addPostFailure: bool
})

export const addPost = ({ title, content }) => {
  return (dispatch) => {
    dispatch(isAddingPost(true))
    return axios.post(`${apiUrl}/posts`, {title, content})
      .then((response) => {
        dispatch(isAddingPost(false))
        const { data } = response
        const newPost = { id: data.id, title: data.title, content: data.content }
        dispatch(addPostSuccess(newPost))
      })
      .then(() => {
        history.push("/")
      })
      .catch(error => {
        dispatch(isAddingPost(false))
        dispatch(addPostFailure(true))
      })
  }
}

7-3 reducers/postReducer.jsを編集する

reducers/postReducer.js

import {
  FETCH_POSTS_SUCCESS,
  IS_FETCHING_POSTS,
  FETCH_POSTS_FAILURE,
  ADD_POST_SUCCESS,
  IS_ADDING_POST,
  ADD_POST_FAILURE
} from '../actions'

...

export function posts(state = [], action) {
  switch (action.type) {
      case FETCH_POSTS_SUCCESS: //このactionがdispatchされたら、action.postsを返すよ~
        return action.posts
      case ADD_POST_SUCCESS:
        return [action.post, ...state]; // ADD_POST_SUCCESSアクションがdispatchされたら、action.postをposts stateの先頭に追加した配列を返すよ~
      default: //なにもされてないときは、デフォルトの[]を返すよ~
        return state;
  }
}

export function isAddingPost(state = false, action) {
  switch (action.type) {
    case IS_ADDING_POST:
      return action.isAddingPost;
    default:
      return state;
  }
}

export function addPostFailure(state = false, action) {
  switch (action.type) {
    case ADD_POST_FAILURE:
      return action.addPostFailure;
    default:
      return state;
  }
}

7-4 reducers/index.jsを編集する

reducers/index.js

import { combineReducers } from 'redux'
import {
  posts,
  fetchPostsFailure,
  isFetchingPosts,
  isAddingPost,
  addPostFailure
} from './postsReducer'

/* 公式ドキュメントのreducersの定義 */
// Reducers specify how the application's state changes in response to actions sent to the store.
// Remember that actions only describe what happened, but don't describe how the application's state changes.

/* これが、src/index.js でstoreと繋がってるのを忘れずに! */
/* storeが意識しずらいのが理解し難い原因かも... */
/* storeをそれ自身ではなく、変化するもの(reducers)で外堀から定義していく */
export default combineReducers({
  posts,
  fetchPostsFailure,
  isFetchingPosts,
  isAddingPost,
  addPostFailure
})

7-5動作確認

してみてください

8 PostShowページの作成

この章のゴール
PostShowページを完成させる
スクリーンショット 2019-05-08 0.29.03.png

8-1 containers/PostShow.jsを編集する

containers/PostShow.js

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getPost } from '../actions'

class PostShow extends Component {
  componentDidMount() {
    this.props.getPost(this.props.match.params.id)
  }

  render() {
    const { post, isFetchingPost, fetchPostFailure } = this.props

    if (isFetchingPost) {
      return (
        <p>Fetching posts...</p>
      )
    }

    if (fetchPostFailure) {
      return (
        <p>Failed to fetch posts...</p>
      )
    }

    return (
      <div>
        <h2>{post.id}: {post.title}</h2>
        <p>{post.content}</p>
        <div>
          <Link to={`/posts/${post.id}/edit`}>
            Edit
          </Link>
        </div>
        <hr/>
      </div>
    )
  }
}

// const mapStateToProps = (state) => ({ post: state.post, isFetchingPost: state.isFetchingPost, fetchPostFailure: state.fetchPostFailure })
/* 上のをかっこよく書くと... */
const mapStateToProps = ({ post, isFetchingPost, fetchPostFailure }) => ({ post, isFetchingPost, fetchPostFailure })

/* mapDispatchToPropsを使ってみる... */
const mapDispatchToProps = { getPost }

export default connect(mapStateToProps, mapDispatchToProps)(PostShow)

8-2 actions/index.jsを編集

actions/index.js

...
export const FETCH_POST_SUCCESS = 'FETCH_POST_SUCCESS'
export const IS_FETCHING_POST = 'IS_FETCHING_POST'
export const FETCH_POST_FAILURE = 'FETCH_POST_FAILURE'

// action creators
const fetchPostSuccess = post => ({
  type: FETCH_POST_SUCCESS,
  post
})

const fetchPostFailure = (bool) => ({
  type: FETCH_POST_FAILURE,
  fetchPostFailure: bool
})

const isFetchingPost = (bool) => ({
  type: IS_FETCHING_POST,
  isFetchingPost: bool
})

// async action creator
export const getPost = (id) => {
  return (dispatch) => {
    dispatch(isFetchingPost(true))
    return axios.get(`${apiUrl}/posts/${id}`)
      .then((response) => {
        dispatch(isFetchingPost(false))
        dispatch(fetchPostSuccess(response.data))
      })
      .catch((error) => {
        dispatch(isFetchingPost(false))
        dispatch(fetchPostFailure(true))
      })
  }
}

8-3 新しくreducers/postReducer.jsを作成する

*postsではなく、postに関連するreducerをまとめるため。
reducers/postReducer.js

import {
  IS_FETCHING_POST,
  FETCH_POST_SUCCESS,
  FETCH_POST_FAILURE
} from '../actions';

export function isFetchingPost(state = false, action) {
  switch (action.type) {
      case IS_FETCHING_POST:
        return action.isFetchingPost;
      default:
        return state;
  }
}

export function post(state = {}, action) {
  switch (action.type) {
      case FETCH_POST_SUCCESS:
        return action.post;
      default:
        return state;
  }
}

export function fetchPostFailure(state = false, action) {
  switch (action.type) {
      case FETCH_POST_FAILURE:
        return action.fetchPostFailure;
      default:
        return state;
  }
}

8-4 reducers/index.jsを編集する

reducers/index.js

import { combineReducers } from 'redux'
import {
  posts,
  fetchPostsFailure,
  isFetchingPosts,
  isAddingPost,
  addPostFailure
} from './postsReducer'

import {
  post,
  fetchPostFailure,
  isFetchingPost
} from './postReducer'

/* 公式ドキュメントのreducersの定義 */
// Reducers specify how the application's state changes in response to actions sent to the store.
// Remember that actions only describe what happened, but don't describe how the application's state changes.

/* これが、src/index.js でstoreと繋がってるのを忘れずに! */
/* storeが意識しずらいのが理解し難い原因かも... */
/* storeをそれ自身ではなく、変化するもの(reducers)で外堀から定義していく */
export default combineReducers({
  posts,
  fetchPostsFailure,
  isFetchingPosts,
  isAddingPost,
  addPostFailure,
  post,
  fetchPostFailure,
  isFetchingPost
})

8-5 動作確認

9 Postを削除できるようにする。

この章のゴール
Postを削除できるようにする。
2019-05-08 01.05.42.gif

9-1 PostShowページを編集する

containers/PostShow.js

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { getPost, deletePost } from '../actions'

class PostShow extends Component {
  componentDidMount() {
    this.props.getPost(this.props.match.params.id)
  }

  render() {
    const { post, isFetchingPost, fetchPostFailure } = this.props

    if (isFetchingPost) {
      return (
        <p>Fetching posts...</p>
      )
    }

    if (fetchPostFailure) {
      return (
        <p>Failed to fetch posts...</p>
      )
    }

    return (
      <div>
        <h2>{post.id}: {post.title}</h2>
        <p>{post.content}</p>
        <div>
          <Link to={`/posts/${post.id}/edit`}>
            Edit
          </Link>
          <button type="button" onClick={() => this.props.deletePost(post.id)}>
            Delete
          </button>
        </div>
        <hr/>
      </div>
    )
  }
}

// const mapStateToProps = (state) => ({ post: state.post, isFetchingPost: state.isFetchingPost, fetchPostFailure: state.fetchPostFailure })
/* 上のをかっこよく書くと... */
const mapStateToProps = ({ post, isFetchingPost, fetchPostFailure }) => ({ post, isFetchingPost, fetchPostFailure })

/* mapDispatchToPropsを使ってみる... */
const mapDispatchToProps = { getPost, deletePost }

export default connect(mapStateToProps, mapDispatchToProps)(PostShow)

9-2 deletePostactions/index.jsで定義する

*isDeletingとかをはしょる
actions/index.js

...
export const DELETE_POST_SUCCESS = 'DELETE_POST_SUCCESS'

const deletePostSuccess = id => ({
  type: DELETE_POST_SUCCESS,
  id
})

export const deletePost = (id) => {
  return (dispatch) => {
    return axios.delete(`${apiUrl}/posts/${id}`)
      .then(response => {
        dispatch(deletePostSuccess(id))
      })
      .then(() => {
        history.push("/")
      })
  }
}

9-3 postsReducer.jsを編集する

import {
...
  DELETE_POST_SUCCESS
} from '../actions'

...
export function posts(state = [], action) {
  switch (action.type) {
      case FETCH_POSTS_SUCCESS: //このactionがdispatchされたら、action.postsを返すよ~
        return action.posts
      case ADD_POST_SUCCESS:
        return [action.post, ...state]; // ADD_POST_SUCCESSアクションがdispatchされたら、action.postをposts stateの先頭に追加した配列を返すよ~
      case DELETE_POST_SUCCESS:
        return state.filter(post => post.id !== action.id);
      default: //なにもされてないときは、デフォルトの[]を返すよ~
        return state;
  }
}

9-4 動作確認

チェックしてください

10 PostをEditできるようにする

*この章のゴール
スクリーンショット 2019-05-08 1.26.44.png

10-1 containers/PostEdit.jsを編集する

import React from 'react';
import { connect } from 'react-redux';
import { getPost, updatePost } from '../actions';

class PostEdit extends React.Component {
  // https://stackoverflow.com/questions/34952530/i-am-using-redux-should-i-manage-controlled-input-state-in-the-redux-store-or-u
  // Local State Or Global State
  constructor(props) {
    super(props)
    this.state = {
      title: '',
      content: ''
    }
  }

  componentDidMount() {
    this.props.getPost(this.props.match.params.id)
      .then(() => {
        const { post } = this.props
        const { title, content } = post
        this.setState({
          title,
          content
        })
      })
  }

  handleChange = (event) => {
    this.setState({ [event.target.name]: event.target.value });
  };

  handleSubmit = (event) => {
    event.preventDefault();
    const id = this.props.post.id;
    const { title, content } = this.state
    const post = { id, title, content }
    this.props.updatePost(post);
  };

  render() {
    const { title, content } = this.state
    return (
      <div>
        <h1>Edit {title}</h1>
        <form onSubmit={this.handleSubmit}>
          <div>
            <label>Title</label>
            <input type="text" name="title" value={title} onChange={this.handleChange} />
          </div>
          <div>
            <label>Content</label>
            <textarea name="content" rows="3" value={content} onChange={this.handleChange} />
          </div>
          <div>
            <button type="submit">Update</button>
          </div>
        </form>
      </div>
    );
  }
}

const mapStateToProps = ({ post }) => ({ post })

const mapDispatchToProps = { updatePost, getPost }

export default connect(mapStateToProps, mapDispatchToProps)(PostEdit)

10-2 定義してない updatePost actionを定義する

actions/index.js

...
export const UPDATE_POST_SUCCESS = 'UPDATE_POST_SUCCESS'

const updatePostSuccess = post => ({
  type: UPDATE_POST_SUCCESS,
  post
})

export const updatePost = (post) => {
  const { id, title, content } = post
  return (dispatch) => {
    return axios.patch(`${apiUrl}/posts/${id}`, {title, content})
      .then(response => {
        const data = response.data
        dispatch(updatePostSuccess(data))
      })
      .then(() => {
        history.push(`/posts/${post.id}`)
      })
  }
}

10-3 postReducer.jsを更新する

postReducer.js

import {
  IS_FETCHING_POST,
  FETCH_POST_SUCCESS,
  FETCH_POST_FAILURE,
  UPDATE_POST_SUCCESS
} from '../actions';
...

export function post(state = {}, action) {
  switch (action.type) {
      case FETCH_POST_SUCCESS:
        return action.post;
      case UPDATE_POST_SUCCESS:
        return action.post;
      default:
        return state;
  }
}

10-4 動作確認

を一旦してください。まだ、終わりじゃないです!

10-5 postsの中のpostを更新する。

*さっきまでは、post単体は更新できたけど、postsの中のpostは更新できていません。
なので、それを実装していきます!

postsReducer.js

import {
...
  UPDATE_POST_SUCCESS
} from '../actions'

...
export function posts(state = [], action) {
  switch (action.type) {
...
      case UPDATE_POST_SUCCESS: // https://hackernoon.com/redux-patterns-add-edit-remove-objects-in-an-array-6ee70cab2456
        return state.map((post) => {
          if (post.id === action.post.id) {
            return {
              ...post,
              ...action.post
            }
          } else return post;
        })
      default: //なにもされてないときは、デフォルトの[]を返すよ~
        return state;
  }
}

終了です!!お疲れ様!!!

パチパチパチパチ!!!!
内容がよかったら、いいねお願いします!

参考サイト

https://redux.js.org/
https://www.techandstartup.com/tutorials/react-redux-crud-app
https://codeburst.io/redux-a-crud-example-abb834d763c9
https://github.com/stowball/dummys-guide-to-redux-and-thunk-react
https://codeburst.io/understanding-redux-thunk-6dbae0241817

12
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13