nodejs + mysql + React + ReduxでCRUDアプリを作る Part2


概要

シンプルなCRUD(create, read, update, delete)アプリをデータベースはmysql, フロントエンドはReact + Reduxで作ってみます.

今回はReact + Reduxでフロントエンドを作ります.

Part1はこちら

なお、今回はこちらの記事をほぼパクらせていただきました. ありがとうございます.


セットアップ

まずcreate-react-appで雛形を作ります.

$ cd crud-node

$ npx create-react-app client
$ cd client

次にpackage.jsonを以下のように書き換えます.


package.json

{

"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.19.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"react-scripts": "3.0.1",
"redux": "^4.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

で、npm installしましょう

$ npm install


reducer

reducerはこのような感じになります.


client/src/reducers/index.js

import { combineReducers } from 'redux'

import {
CHANGE_NAME, CHANGE_STATUS, INITIALIZE_FORM,
REQUEST_DATA, RECEIVE_DATA_SUCCESS, RECEIVE_DATA_FAILED
} from '../actions';

const initialState = {
form: {
name: '',
status: '',
ustatus: ''
},
users: {
isFetching: false,
users: []
}
}

const formReducer = (state = initialState.form, action) => {
switch(action.type) {
case CHANGE_NAME:
return {
...state,
name: action.name
}
case CHANGE_STATUS:
return {
...state,
status: action.status
}
case INITIALIZE_FORM:
return initialState.form
default:
return state
}
}

const usersReducer = (state = initialState.users, action) => {
switch(action.type) {
case REQUEST_DATA:
return {
...state,
isFetching: true
}
case RECEIVE_DATA_SUCCESS:
return {
...state,
isFetching: false,
users: action.users
}
case RECEIVE_DATA_FAILED:
return {
...state,
isFetching: false
}
default:
return state;
}
}

const rootReducer = combineReducers({
form: formReducer,
users: usersReducer
})

export default rootReducer;


入力フォーム用のformReducerとデータベースの読み取りの時用のusersReducerを作って, rootReducerでまとめています.


action

actionは以下のような感じになります.


client/src/actions/index.js

export const CHANGE_NAME = 'CHANGE_NAME';

export const CHANGE_STATUS = 'CHANGE_STATUS';
export const CHANGE_USTATUS = 'CHANGE_USTATUS';
export const INITIALIZE_FORM = 'INITIALIZE_FORM';
export const REQUEST_DATA = 'REQUEST_DATA';
export const RECEIVE_DATA_SUCCESS = 'RECEIVE_DATA_SUCCESS';
export const RECEIVE_DATA_FAILED = 'RECEIVE_DATA_FAILED';

export const changeName = (name) => ({
type: CHANGE_NAME,
name
})

export const changeStatus = (status) => ({
type: CHANGE_STATUS,
status
})

export const initializeForm = () => ({
type: INITIALIZE_FORM
})

export const requestData = () => ({
type: REQUEST_DATA
})

export const receiveDataSuccess = (users) => ({
type: RECEIVE_DATA_SUCCESS,
users
})

export const receiveDataFailed = () => ({
type: RECEIVE_DATA_FAILED
})


changeUstatus はstatus更新のための入力フォーム用です.


store

storeを作ります.


client/src/index.js

import React from 'react'

import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import App from './components/App'
import rootReducer from './reducers'
import { Provider } from 'react-redux'

const store = createStore(rootReducer);

console.log(store);

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



component

componentは全部で3つでApp.jsが親コンポーネント, Form.jsList.jsがその子コンポーネントという構成になっています.

なお, 今回はmapDispatchToPropsmapStateToPropsを使いたかったので非常に回りくどい書き方をしております.

シンプルに書きたい方は上で紹介した記事をご参照ください.


client/src/compoents/App.js

import React, { Component } from 'react';

import { connect } from 'react-redux';
import { changeName, changeStatus, initializeForm, requestData, receiveDataSuccess, receiveDataFailed } from '../actions';
import Form from './Form';
import List from './List';

class App extends Component {
render() {
return (
<div>
<Form
changeName={this.props.changeName}
changeStatus={this.props.changeStatus}
initializeForm={this.props.initializeForm}
requestData={this.props.requestData}
receiveDataSuccess={this.props.receiveDataSuccess}
receiveDataFailed={this.props.receiveDataFailed}
name={this.props.name}
status={this.props.status}
/>
<List
initializeForm={this.props.initializeForm}
requestData={this.props.requestData}
receiveDataSuccess={this.props.receiveDataSuccess}
receiveDataFailed={this.props.receiveDataFailed}
isFetching={this.props.isFetching}
ustatus={this.props.ustatus}
users={this.props.users}
/>
</div>
)
}
}

const mapDispatchToProps = ({ changeName, changeStatus, initializeForm, requestData, receiveDataSuccess, receiveDataFailed });

const mapStateToProps = state => ({ name: state.form.name, status: state.form.status, users: state.users.users, isFetching: state.users.isFetching });

export default connect(mapStateToProps,mapDispatchToProps)(App)


以下はユーザーを登録するためのFormコンポーネントです.


client/src/compoents/Form.js

import React from 'react';

import axios from 'axios';

const ROOT_ENDPOINT = 'http://localhost:3001';

const Form = ({ name, status, changeName, changeStatus, initializeForm, requestData, receiveDataSuccess, receiveDataFailed }) => {
const createUser = e => {
if(name.length > 10 || status.length > 10) {
alert('文字数が多いです');
} else {
e.preventDefault();
axios({
method: 'post',
url: ROOT_ENDPOINT + '/user/create',
data: {
name: name,
status: status
}
})
.then(res => {
initializeForm();
const _users = res.data;
console.log(_users);
receiveDataSuccess(_users);
})
.catch(err => {
console.log(err);
alert('登録に失敗しました')
receiveDataFailed();
})
}
}

return (
<div>
<form onSubmit={e => createUser(e)}>
<label>
name:
<input value={name} onChange={e => changeName(e.target.value)} />
</label>
<label>
status:
<input value={status} onChange={e => changeStatus(e.target.value)} />
</label>
<button type="submit">register</button>
</form>
</div>
)
}

export default Form;


以下はユーザーを表示, 更新, 削除するためのListコンポーネントです.


client/src/compoents/List.js

import React from 'react';

import axios from 'axios';

const ROOT_ENDPOINT = 'http://localhost:3001';

class List extends React.Component {
constructor(props) {
super(props);
}

componentDidMount = () => {
this.fetchData();
}

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

fetchData = () => {
this.props.requestData();
axios.get(ROOT_ENDPOINT + '/user')
.then(res => {
const _users = res.data;
this.props.receiveDataSuccess(_users);
})
.catch(err => {
console.log(err);
this.props.receiveDataFailed();
})
}

updateUser = (id) => {
if(this.state[`${id}`].length > 10) {
alert('文字数が多いです');
} else {
this.props.requestData();
axios({
method: 'put',
url: ROOT_ENDPOINT + '/user/update',
data: {
id: id,
status: this.state[`${id}`]
}
})
.then(res => {
const _users = res.data;
this.props.receiveDataSuccess(_users);
})
.catch(err => {
console.log(err);
alert('更新に失敗しました');
this.props.receiveDataFailed();
})
}
}

deleteUser = (id) => {
this.props.requestData();
axios({
method: 'delete',
url: ROOT_ENDPOINT + '/user/delete',
data: {
id: id
}
})
.then(res => {
const _users = res.data;
this.props.receiveDataSuccess(_users);
})
.catch(err => {
console.log(err);
alert('削除に失敗しました');
this.props.receiveDataFailed();
})
}

render() {
return (
<div>
{
this.props.isFetching
? <h2>Now Loading...</h2>
: <div>
<ul>
{this.props.users.map(user => (
<li key={user.id}>
{`${user.name}: ${user.status}`}
<input onChange={this.handleChange(`${user.id}`)} />
<button onClick={() => this.updateUser(user.id)}>update</button>
<button onClick={() => this.deleteUser(user.id)}>delete</button>
</li>
))}
</ul>
</div>
}
</div>
)
}
}

export default List;


ソースコードはこちらです

https://github.com/melonattacker/crud-app


デモ

out.gif


Happy Hacking :sunglasses: !