はじめに。 なぜ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.rb
のdef 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
にアクセス
この章はこれで、終了です。
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を使うとき公式ではどうなっているかというと、下図のようになっている...
(https://redux.js.org/advanced/usage-with-react-router#indexjs)
またはredux-thunkを使った時のexampleでは下図のようになっている,
(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.js
とrootReducer
を作る必要があるので作っていきましょう.
4-3 src/components/Root.js
Root.js
は公式ドキュメントでは下図の様な感じ(https://redux.js.org/advanced/usage-with-react-router#components-rootjs)
これを真似て、
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)...
これを真似て...
src/reducers/index.js
import { combineReducers } from 'redux'
export default combineReducers({
/* あとで追加 */
})
4-5 動作確認!
動くか確認してください!!
Routingがきっちりできていれば大丈夫です!
*では、次の章からは、本題のReduxの使い方になっていきます! 気合いいれていきましょう!
*基本的には、ファイルをコピペしてもらって、
1. 流れを掴む
2. コメントを読んで、少し理解する
3. 納得いかない箇所を自分で調べる
という感じで進めていってもらえると助かります!
5 トップページでPostsリストを表示させる
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を分けています。
これについては、あとで触れます!
https://redux.js.org/advanced/async-actions#actions
5-3 reduces/index.js
と reducers/postsReducers.js
reducers/index.js
公式ドキュメントでは...
では、これらを参考に書いていきましょう。
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
5-4 動作確認
ここで、一度立ち止まって確認してみてください!
6 エラーハンドリングをする
この章でやること
以下の様なエラーハンドリングをする。
https://redux.js.org/advanced/async-actions#actions
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
*これで、fetchPostsFailure
とisFetchingPosts
が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ページを完成させる
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ページの作成
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を削除できるようにする。
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 deletePost
をactions/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できるようにする
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