Reduxを使った転勤の傾向アプリ

Tatsuya Asami
41 min readNov 2, 2018

--

【所要時間】

30時間くらい(2018年11月1日完成)

【概要】

狙い

・Reduxを使いデータを管理する。

・フィルター機能、機能を使う。

・React-routerを導入する。

・完成したページ
https://astatsuya.github.io/transfer/

【要約・学んだこと】

制作の流れ

  1. Redux部分を作成。
  2. Reactでページ構成。
  • トップページ
  • 入力ページ
  • データのリストが観覧できるページ
  • 検索ページ

3. ReactとReduxを組み合わせる。

4. 表示したい情報を追加、修正する。

5. React routerで表示切り替え。

6. レイアウト等細かい修正。

  1. 入力フォームから送る情報を取り扱う。

actionは

//redux/actions/action.jsexport const ADD_INFO = 'ADD_INFO';export const addInfo = info => ({
type: ADD_INFO,
info,
});

reducerは

//redux/reducers/reducer.jsimport { ADD_INFO } from '../actions/action';const initialState = {
info: [
{
name: 'Tom',
age: 42,
department: 'Marketing',
position: 'Manager',
join: 2015,
leave: 2017,
branch: 'Tokyo',
},
],
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_INFO:
return {
...state,
info: [
...state.info,
action.info,
],
};
default:
return state;
}
};
export default rootReducer;

試しに実行すると、正しく取得できた。

この際、下記のコードをindex.jsに追記し、console画面でstoreのstateの状態を取得できるようにした。

import store from "./redux/store.js";
import { addInfo } from "./redux/actions/action.js";

window.store = store;
window.addInfo = addInfo;

2. Reactでページ構成

//src/App.jsimport React from 'react';
import Form from './components/Form';
const App = () => (
<div>
app
<Form />
</div>
);
export default App;

Form.jsではmapDispatchToPropsで、Formに打ち込んだ内容をReduxに送信するようにする。とりあえずname, ageだけを作り、試しにdispatchしてみる。

import React from 'react';
import { connect } from 'react-redux';
import { addInfo } from '../redux/actions/action';
const mapDispatchToProps = (dispatch) => {
return {
addInfo: info => dispatch(addInfo(info)),
};
};
class ConnectedForm extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
age: '',
};
this.handleNameChange = this.handleNameChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleNameChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value,
});
}
handleSubmit(event) {
event.preventDefault();
const { name, age } = this.state;
this.props.addInfo({ name, age });
this.setState({
name: '',
age: '',
});
}
render() {
const { name, age } = this.state;
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="Name">
Name:
<input
type="text"
name="name"
value={name}
onChange={this.handleNameChange}
/>
</label>
<label htmlFor="age">
age:
<input
type=""
name="age"
value={age}
onChange={this.handleNameChange}
/>
</label>
<button type="submit" value="Send">
submit
</button>
</form>
</div>
);
}
}
const Form = connect(null, mapDispatchToProps)(ConnectedForm);export default Form;

接続できた。

Formを仕上げていく。

// components/Form.jsimport React from 'react';
import { connect } from 'react-redux';
import { addInfo } from '../redux/actions/action';
const mapDispatchToProps = dispatch => {
...
};
class ConnectedForm extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
age: 30,
arrival: 2010,
leave: 2010,
gender: 'Male',
department: 'Marketing',
position: 'Director',
location: 'Tokyo',
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClear = this.handleClear.bind(this);
}
handleChange(event) {
...
}
handleSubmit(event) {
event.preventDefault();
const {
name, age, gender, department, position, arrival, leave, location
} = this.state;
if(name === "") {
alert('You must input your name!!');
} else if(arrival > leave) {
alert('Arrival should be earlier than Leaving');
} else {
this.props.addInfo({
name, age, gender, department, position, arrival, leave, location
});
this.setState({
name: '',
age: 30,
arrival: 2010,
leave: 2010,
gender: 'Male',
department: 'Marketing',
position: 'Director',
location: 'Tokyo',
});
}
}
handleClear(event) {
event.preventDefault();
this.setState({
name: '',
age: 30,
arrival: 2010,
leave: 2010,
gender: 'Male',
department: 'Marketing',
position: 'Director',
location: 'Tokyo',
});
}
render() {
const {
name, age, gender, department, position, arrival, leave, location
} = this.state;
return (
<form className="form" onSubmit={this.handleSubmit}>
...
Gender
</label>
<br />
<select name="gender" onChange={this.handleChange} value={gender}>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Others">Others</option>
</select>
<br />
... Arrival
</label>
<br />
<input
type="number"
name="arrival"
value={arrival}
onChange={this.handleChange}
/>
<br />
...<button type="submit" value="Send">
submit
</button>
<button onClick={this.handleClear}>
Reset
</button>
</form>
);
}
}
const Form = connect(null, mapDispatchToProps)(ConnectedForm);export default Form;

これで一通りできた。submitを押すとFormに入力した内容が、action addInfoの内容となりdispatchされる。Resetを押すと、Formの内容が初期状態になる。

次にDatabase.jsを作成する。ここではStateの内容を一覧表示する。

まずはReduxとReactを接続し、画面に表示してみる。

// components/Database.jsimport React from 'react';
import { connect } from 'react-redux';
const mapStateToProps = state => {
return {
info: state.info,
};
};
const ConnectedDataBase = ({ info }) => (
<table>
<tr>
<th>name</th>
<th>age</th>
</tr>
<tr>
<td>{info[0].name}</td>
<td>{info[0].age}</td>
</tr>
</table>
);
const DataBase = connect(mapStateToProps)(ConnectedDataBase);export default DataBase;

うまく繋がった。

次はReduxのstate全ての内容を一覧表にしたい。

react-bootstrap-tableを使おうとしたが、うまくいかなかったので諦め、手動で表を作成する。

// components/Database.jsimport React from 'react';
import { connect } from 'react-redux';
const mapStateToProps = state => {
return {
columns: state.columns,
info: state.info,
};
};
const ConnectedDataBase = ({ columns, info }) => (
<table>
<tr>
{columns.map(state => (
<th>{state}</th>
))}
</tr>
{info.map((state, index) => {
return (
<tr>
<td>{state.name}</td>
<td>{state.age}</td>
<td>{state.gender}</td>
<td>{state.department}</td>
<td>{state.position}</td>
<td>{state.arrival}</td>
<td>{state.leave}</td>
<td>{state.location}</td>
</tr>
);
})}
</table>
);
const DataBase= connect(mapStateToProps)(ConnectedDataBase);export default DataBase;

Reduxには新たにcolumnsというinitialStateを追加。

// redux/reducers/reducer.jsconst initialState = {
columns: ['name', 'age', 'gender', 'department', 'position', 'arrival', 'leave', 'location'],
info: [
{
name: 'Tom',
age: 42,
gender: 'Male',
department: 'Marketing',
position: 'Manager',
arrival: 2015,
leave: 2017,
location: 'Tokyo',
},
],
};

これで登録したデータが表示される。(この辺からスクショ全然取ってない。)完成後は下記のようにstateの内容が表示されている。

/components/Database.js

ここまででReactとReduxが接続され、ブラウザからReduxデータの取得、更新ができることを確認した。

4. 表示したい情報を追加、修正する。

Redux側でstateにフィルターをかけようとしたが、イマイチやり方がわからず断念し、React側で表示する内容にフィルターをかけた。

  • 全員の異動までの平均年数と、該当者(性別、部署、役職など選べる)の平均年数
  • 全員の異動先と、該当者の異動先

を表示したいので、Searchform.jsを作成。

ここではstateで検索条件を保持する。セレクターでstate内容を変更するシンプルなclass componentを作成。

//components/Searchform.jsimport React from 'react';
import Search from './Search';
class SearchForm extends React.Component {
constructor(props) {
super(props);
this.state = {
gender: 'all',
department: 'all',
position: 'all',
location: 'all',
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value,
})
}
render() {
const {
gender, department, position, location
} = this.state;
return (
<form className="searchform" onSubmit={this.handleSubmit}>
<label>
Gender:
</label>
<select name="gender" onChange={this.handleChange} value={gender}>
<option value="all">All</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Others">Others</option>
</select>
<br />
<label>
Department:
</label>
<select name="department" onChange={this.handleChange} value={department}>
<option value="all">All</option>
<option value="Marketing">Marketing</option>
<option value="Engineering">Engineering</option>
<option value="Others">Others</option>
</select>
<br />
<label>
Position:
</label>
<select name="position" onChange={this.handleChange} value={position}>
<option value="all">All</option>
<option value="Director">Director</option>
<option value="Manager">Manager</option>
<option value="Chief">Chief</option>
<option value="Others">Others</option>
</select>
<br />
<label>
Location:
</label>
<select name="location" onChange={this.handleChange} value={location}>
<option value="all">All</option>
<option value="Tokyo">Tokyo</option>
<option value="Osaka">Osaka</option>
<option value="OverSeas">OverSeas</option>
<option value="Others">Others</option>
</select>
<br />
<br />
<br />
<Search
gender={this.state.gender}
department={this.state.department}
position={this.state.position}
location={this.state.location}
/>
<br />
</form>
);
}
}
export default SearchForm;

検索結果はSearch.jsという子コンポーネントで表示するようにする。

Searchform.js

Search.js で、Reduxのstateを受け取り、表示する情報を操作する。

//components/Search.jsimport React from 'react';
import { connect } from 'react-redux';
const mapStateToProps = (state, ownProps) => {
return {
columns: state.columns,
info: state.info,
ownProps: ownProps
};
};
const sum = a => {
return a.reduce((x, y) => {
return x + y;
}, 0)
}
const length = a => {
return a.reduce((x, y) => {
return a.length;
}, 0)
}
const ConnectedSearch = ({ info, ownProps } ) => {
//男性か女性か
let selectedGender = ownProps.gender;
const filterGender = info.filter(value => {
if(selectedGender !== 'all'){
return value.gender === selectedGender;
} else {
return value.gender
}
})
//その中で部署はどこか
let selectedDepartment = ownProps.department
const filterDepartment = filterGender.filter(value => {
if(selectedDepartment !== 'all'){
return value.department === selectedDepartment;
} else {
return value.department
}
})
//その中でポジションは何か
let selectedPosition = ownProps.position;
const filterPosition = filterDepartment.filter(value => {
if(selectedPosition !== 'all'){
return value.position === selectedPosition;
} else {
return value.position
}
})
//その中でロケーションはどこか
let selectedLocation = ownProps.location;
const filterLocation = filterPosition.filter(value => {
if(selectedLocation !== 'all'){
return value.location === selectedLocation;
} else {
return value.location
}
})
//選択した部署のLeave - Arrival
const term_filter_array = filterPosition.map(a => {
return a.leave - a.arrival;
})
//選択した部署の配列のposition要素の羅列
const length_filter_array = filterPosition.map(a => {
return a.position
})
//全員のLeave - arrivalの列挙
const term_stay_each = info.map(a => {
return a.leave - a.arrival;
})
//全員の名前の列挙
const nameAll = info.map(a => {
return a.name
})
//全員のLocationの列挙
const locationAll = info.map(a => {
return a.location
})
//全員の配列の要素数
const lengthArrayAll = length(nameAll)
//全員の平均勤続年数 = (全員のleave-arrivalの合計) / 該全員の配列の要素数
const term_stay_average = (sum(term_stay_each) /lengthArrayAll).toFixed(1) | 0;
//全員の異動先毎の人数
const new_location_all = () => {
const lengthOfTokyo = locationAll.filter((array) => {
return array === 'Tokyo'
}).length
const lengthOfOsaka = locationAll.filter((array) => {
return array === 'Osaka'
}).length
const lengthOfOverSeas = locationAll.filter((array) => {
return array === 'OverSeas'
}).length
const lengthOfOthers = locationAll.filter((array) => {
return array === 'Others'
}).length
return `東京: ${lengthOfTokyo}人
大阪: ${lengthOfOsaka}人
海外: ${lengthOfOverSeas}人
その他: ${lengthOfOthers}人`
}
//該当者の配列の要素数
const lengthArrayFiltered = length(length_filter_array)
//該当者の平均勤続年数 = (該当者のleave-arrivalの合計) / 該当者の配列の要素数
const term_stay_filtered = (sum(term_filter_array) / lengthArrayFiltered).toFixed(1) | 0;
//該当者の異動先の列挙
const new_location = filterLocation.map(a => {
return a.location;
})
//該当者の異動先毎の人数
const new_location_filtered = () => {
const lengthOfTokyo = new_location.filter((array) => {
return array === 'Tokyo'
}).length
const lengthOfOsaka = new_location.filter((array) => {
return array === 'Osaka'
}).length
const lengthOfOverSeas = new_location.filter((array) => {
return array === 'OverSeas'
}).length
const lengthOfOthers = new_location.filter((array) => {
return array === 'Others'
}).length
return `東京: ${lengthOfTokyo}人
大阪: ${lengthOfOsaka}人
海外: ${lengthOfOverSeas}人
その他: ${lengthOfOthers}人`
}
return (
<div className='searchresult'>
全人数 : {lengthArrayAll}人
<br />
異動までの平均年数(全体) : {term_stay_average}年
<br />
異動先(全体){new_location_all()}
<br />
<br />
該当者数 : {lengthArrayFiltered}人
<br />
異動までの平均年数(該当者) : {term_stay_filtered}年
<br />
異動先(該当者){new_location_filtered()}
<br />
</div>
);
};
const Search = connect(mapStateToProps)(ConnectedSearch);export default Search;完成後はこのように表示される。

部分的にみていくと

const ConnectedSearch = ({ info, ownProps } ) => {
//男性か女性か
let selectedGender = ownProps.gender;
const filterGender = info.filter(value => {
if(selectedGender !== 'all'){
return value.gender === selectedGender;
} else {
return value.gender
}
})
//その中で部署はどこか
let selectedDepartment = ownProps.department
const filterDepartment = filterGender.filter(value => {
if(selectedDepartment !== 'all'){
return value.department === selectedDepartment;
} else {
return value.department
}
})

infoがReduxのstate、ownPropsがSearchform.jsから受け取ったstateである。

Searchform.jsから受け取るstate.genderがallの場合は、そのまま配列の要素を返し、それ以外の場合は一致する要素のみを返す。

次はgenderでフィルタリングした後の配列から、さらstate.departmentで一致する条件のみを返す。

これを繰り返していくと、4つの条件でフィルタリングできる。

異動先の列挙は下記のようにして取得する。

//該当者の異動先の列挙
const new_location = filterLocation.map(a => {
return a.location;
})
//該当者の異動先毎の人数
const new_location_filtered = () => {
const lengthOfTokyo = new_location.filter((array) => {
return array === 'Tokyo'
}).length
const lengthOfOsaka = new_location.filter((array) => {
return array === 'Osaka'
}).length
const lengthOfOverSeas = new_location.filter((array) => {
return array === 'OverSeas'
}).length
const lengthOfOthers = new_location.filter((array) => {
return array === 'Others'
}).length
return `東京: ${lengthOfTokyo}人
大阪: ${lengthOfOsaka}人
海外: ${lengthOfOverSeas}人
その他: ${lengthOfOthers}人`
}

new_locationで、異動先の勤務地を列挙している。(配列のlocation要素のみ表示)

new_location_filteredで、location毎の人数を数えている。

これらの方法でフィルターできるようにした。

これで表示する内容は完成。次にReact routerでブラウザ上の表示切り替えの設定を行う。

5. React routerで表示切り替え。

画面上部にリンクバーを設置し、切り替えられるようにする。

App.jsは下記のようにした。

//App.jsimport React from 'react';
import Form from './components/Form';
import DataBase from './components/Database';
import SearchForm from './components/Searchform';
import PageContents from './components/PageContents';
import Topbar from './components/Topbar';
import './App.css';
import { BrowserRouter, Route } from 'react-router-dom'
const App = () => (
<BrowserRouter>
<div className="main-page">
<Topbar />
<Route path={process.env.PUBLIC_URL + '/'} exact component={PageContents} />
<Route path="/form" component={Form} />
<Route path="/search" component={SearchForm} />
<Route path="/database" component={DataBase} />
</div>
</BrowserRouter>
)
export default App;

BrowserRouterで表示する内容を囲む。

<Route path={process.env.PUBLIC_URL + '/'} exact component={PageContents} /><Route path="/form" component={Form} />

上記のように、pathにURLを指定。componentに表示するコンテンツ(クラス名)を指定。
トップページはexact component={PageContents} とexactをつける。また、今回はGithub pagesで表示したいので、pathもpath={process.env.PUBLIC_URL + ‘/’}とする必要があった。

次にTopbarで、ボタンをクリックするとページが異動できるようにする。

import React from 'react';
import LinkButton from './LinkButton';
import { Link } from 'react-router-dom'
const Topbar = () => (
<div className="topbar">
<h3>社員データ</h3>
<Link to={process.env.PUBLIC_URL + '/'}>
<LinkButton buttonName={'トップ'} />
</Link>
<Link to="/form/">
<LinkButton buttonName={'情報入力'} />
</Link>
<Link to="/search/">
<LinkButton buttonName={'情報検索'} />
</Link>
<Link to="/database/">
<LinkButton buttonName={'データベース'} />
</Link>
</div>
);
export default Topbar;

こちらもシンプルに

<Link to="/form/">
<LinkButton buttonName={'情報入力'} />
</Link>

Link to =”/form/” のように異動先のURLを指定し、リンクボタンをその中に記載する。

最後にレイアウトなどを修正して完成。https://astatsuya.github.io/transfer/

【苦戦し、新たに学んだところ】

  • Reduxのstateを受け取るクラスに、Reactの別のコンポーネントからもstateを渡すには、ownPropsで渡す。

[mapStateToProps(state, [ownProps]): stateProps] (Function): If this argument is specified, the new component will subscribe to Redux store updates. This means that any time the store is updated, mapStateToProps will be called. The results of mapStateToProps must be a plain object, which will be merged into the component’s props. If you don't want to subscribe to store updates, pass null or undefined in place of mapStateToProps.

const mapStateToProps = (state, ownProps) => {
return {
columns: state.columns,
info: state.info,
ownProps: ownProps
};
};
const ConnectedSearch = ({ info, ownProps } ) => {

let selectedGender = ownProps.gender;
・・・・・

これで親コンポーネントのstate.genderが取得できる。

  • 複数のstateを同じように操作(onClickやonChange)したい場合は下記のように記載できる。項目の追加、変更にも対応できる。
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;

this.setState({
[name]: value
});
}

render() {
return (
<form>
<label>
Is going:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests"
  • 初期stateの値を指定していないと、初期で選択している項目がundefinedになる。
  • inputの中身はtypeをnumberにしても、stringとして渡される(age: 19 ではなく age: “19” となってしまう)ので、parseIntでnumberのままにする必要がある。

https://gomakethings.com/converting-strings-to-numbers-with-vanilla-javascript/

handleSubmit(event) {
event.preventDefault();
let {
name, age, gender, department, position, arrival, leave, location
} = this.state;
age = parseInt(age, 10);
arrival = parseInt(arrival, 10);
leave = parseInt(leave, 10);
  • このようにすると、一度フィルターをかけることができるが、その後filterを変更すると、stateの中身が元に戻らない。(Engineeringでフィルターをかけると、もう一度フィルターをかける際、Engineering以外の配列は失われる。)
case FILTER_STATE:
switch(action.filter) {
case 'all':
return state;
case 'Marketing':
return Object.assign({}, state, {
info: state.info.filter(value => {
return value.department === 'Marketing'
}),
filter: action.filter
})
case 'Engineering':
return Object.assign({}, state, {
info: state.info.filter(value => {
return value.department === 'Engineering'
}),
filter: action.filter
})
case 'Others':
return Object.assign({}, state, {
info: state.info.filter(value => {
return value.department === 'Others'
}),
filter: action.filter
})
default:
return state;
}
  • 配列内で条件に一致する要素の数をカウントしようとしたがうまくいかなかった。
const percentage_location = () => {
let num_Tokyo = 0;
let num_Osaka = 0;
let num_OverSeas = 0;
let num_Others = 0;
for(let i = 0; i < new_location.length; i++) {
if(new_location[i] === 'Tokyo') {
num_Tokyo++;
continue;
} else if (new_location[i] === 'Osaka') {
num_Osaka++;
continue;
} else if (new_location[i] === 'OverSeas') {
num_OverSeas++;
continue;
} else {
num_Others++;
}
console.log( `東京は${num_Tokyo}人、大阪は${num_Osaka}人、海外は${num_OverSeas}人、その他${num_Others}人
`)
}
}

こっちでやった。

const new_location_all = () => {
const lengthOfTokyo = locationAll.filter((array) => {
return array === 'Tokyo'
}).length
const lengthOfOsaka = locationAll.filter((array) => {
return array === 'Osaka'
}).length
const lengthOfOverSeas = locationAll.filter((array) => {
return array === 'OverSeas'
}).length
const lengthOfOthers = locationAll.filter((array) => {
return array === 'Others'
}).length
return `東京: ${lengthOfTokyo}人
大阪: ${lengthOfOsaka}人
海外: ${lengthOfOverSeas}人
その他: ${lengthOfOthers}人`
}
  • github pagesに更新した情報が反映されるまでに数分以上のタイムラグがあると思う。
    CSSでfontsizaなどの軽微なを変更したら、CSSが全く読み込まれなくなったりした。変更内容が絶対おかしくならなそうな内容だったので、ちょっと休憩して調べようとしたら、問題なくなっていたことに気が付いたが、変更内容によってははまっていたと思う。
  • React routerをgithub pagesに反映させるには<Route path={process.env.PUBLIC_URL + ‘/’}>をルートアドレスにする。他のやり方もある。

Use process.env.PUBLIC_URL in your route definitions so that they work both in development and after deployment. For example: <Route path={process.env.PUBLIC_URL + '/'}>. This will be empty in development and rcws-development (inferred from homepage) in production.

【わからなかったこと】

  • Reduxのstateの内容をうまくソート、フィルタリングする方法。

うまく記述できなかったので、次回やる。

  • React-bootstrap-table2

うまくインストールできなかったので今回はパスした。

【感想】

すぐ終わると思ったら各所でつまり続けてとても時間がかかった。

だが以前と比べると、間違っていない確信が持てる場所が増え、また原因の切り分けも少しづつ早くできるようになっている。

ツールの導入の場合は、ちょっとググってもうまくいかなかったら、すぐにcreate-react-appで新しいプロジェクトをつくり、そこで試してみるのが一番早く解決できそうだと思った。

また、調べる際も、githubや公式を最初にみた方が、現段階では早いと思った。(今回はownPropsの部分など、基本的なルールだった。)

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

No responses yet