背景
Reactを使ってWebアプリを開発していく中で、ある程度作り方が自分の中で固まってきたので、この記事を書くことにしました。
概要
ReactのComponentは、classを使用せず**関数化SFC(Stateless Function Component)化させることで、パフォーマンス向上、ロジックとビューの分離、可読性の向上を図ることができます。
またComponentを関数化SFC(Stateless Function Component)化してしまうと、ライフサイクルメソッドを使えなくなります。。
こちらはHoC(React Higher-order Component)という、Componentを引数に、機能を追加したComponentを返す関数を利用することでライフサイクルメソッドの問題は解決できます
→ReactのComponentは、classを使用せずHooksを利用してFC(Function Component)化させることでパフォーマンス向上、ロジックとビューの分離、可読性の向上を図ることができます。(2019/3/10 追記)
※HoCを利用する必要はなくなりました。
ActionとReducerはFSA(Flux Standard Action)のコーディング規則に沿って実装をします。ただし、FSAライブラリを使用することでコーディング量をできる限り削減し、可読性向上を図った書き方ができるのでそちらをご紹介します。
※加えて、タスクという概念を加えることで、Actionをビューで使用するActionとReducerで使用するActionに切り分けてメンテ性を向上させる手法が好きなのだが、ここでは割愛します。
対象者
reactとreduxを使ってReactアプリを作れる方
typescriptがわかる方
material-uiの使い方をある程度知っている方
環境構築
まずはcreate-react-app
で、typescript用のReactアプリを作成します。
$ npm install create-react-app -g
$ create-react-app my-app --scripts-version=react-scripts-ts
my-appというフォルダが作成されているはずです。
my-appに移動してnpm start
で、Reactアプリが起動するか確認します。
次に、必要なライブラリをインストールします。
Library | version |
---|---|
redux | v4.0.1 |
react-redux | v6.0.0 |
recompose | v0.30.0 |
typescript-fsa | v3.0.0-beta-2 |
typescript-fsa-reducers | v1.1.0 |
@material-ui/core | v3.6.1 |
@material-ui/icons | v3.0.1 |
※本当はredux-sagaも導入したいが、好みが分かれると思うのでそちらは別の記事で説明します。
npm install redux --save
npm install react-redux --save
npm install recompose --save
npm install typescript-fsa --save
npm install typescript-fsa-reducers --save
npm install @material-ui/core --save
npm install @material-ui/icons --save
ライブラリがtypescriptで動くように、typescript用のライブラリもインストールします。
Library | version |
---|---|
@types/redux | v3.6.0 |
@types/react-redux | v6.0.10 |
@types/recompose | v0.30.0 |
npm install @types/redux --save-dev
npm install @types/react-redux --save-dev
最後にtslint.jsonの設定を変えます。
my-app直下にtslint.jsonがあるので、以下のようにルールを設定してください。ルールに関してはわからない方はgoogleで検索するなりして、理解しておいてください。
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"coverage/lcov-report/*.js"
]
},
"rules": {
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-pascal-case",
"allow-leading-underscore"
],
"no-console": [false],
"import-name": false,
"ordered-imports": false,
"interface-name": false,
"no-empty-interface": false,
"object-literal-sort-keys": false,
"object-literal-shorthand": false,
"jsx-no-lambda": false,
"linebreak-style": false
}
}
手順
フォルダの作成
まず「components」、「containers」、「actions」、「reducers」、「helpers」フォルダを作成します。
my-app\src
├── actions
├── App.css
├── App.test.tsx
├── App.tsx
├── components
├── containers
├── helpers
├── index.css
├── index.tsx
├── logo.svg
├── reducers
└── registerServiceWorker.ts
Componentの作成
まずは、components内にhome.tsxというComponentを作成します。
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
interface OwnProps {}
const styles = (theme: Theme) => {
return createStyles({
layout: {
marginTop: 100,
padding: theme.spacing.unit * 2,
maxWidth: 1200,
[theme.breakpoints.up(1200)]: {
marginLeft: 'auto',
marginRight: 'auto',
},
},
})
}
type Props = OwnProps & WithStyles<typeof styles>
const HomeComponent: React.SFC<Props> = (props: Props) => {
const { classes } = props
return (
<div>
<AppBar>
<Toolbar>
<Typography color='inherit'>Hello World</Typography>
</Toolbar>
</AppBar>
<div className={classes.layout}>
<Typography>this is home component</Typography>
<Button>Open</Button>
</div>
</div>
)
}
export default withStyles(styles)(HomeComponent)
いつも何も考えず使っているであろうprops
はtypescriptでは定義してあげないと使えません。なのでProps
という型を作成します。
またmaterial-uiのレイアウト変えるstylesはcreateStyles
を使用して作成し、WithStyles
を使用して型をProps
に渡します。
意外とtypescriptでmaterial-uiを使用する際、styles
の指定の仕方が書いてある記事がないです。詳しい記事があればリンクを張りたいので教えて下さい。
※withStyles
ではなく、ここで使用するのはWithStyles
です。
interface OwnProps {}
const styles = (theme: Theme) => {
return createStyles({
layout: {
marginTop: 100,
padding: theme.spacing.unit * 2,
maxWidth: 1200,
[theme.breakpoints.up(1200)]: {
marginLeft: 'auto',
marginRight: 'auto',
},
},
})
}
type Props = OwnProps & WithStyles<typeof styles>
今回はSFC(Stateless Function Component)のコンポーネントを作成するので、class
は使用しません。
ReactはSFC(Stateless Function Component)を作成するAPIを用意してくれています。
先ほど定義したProps
を型に利用して、props
を引数にとるHomeComponent
を書きます。
const HomeComponent: React.SFC<Props> = (props: Props) => {
const { classes } = props
return (
// Componentの中身
)
}
最後はいつも通り、material-uiお決まりのwithStyles
を使用してdefault export
しておきます。
export default withStyles(styles)(HomeComponent)
HoC(React Higher-order Component)の作成
Componentのライフサイクルメソッドを利用できるようHoC(React Higher-order Component)を作成します。
HoC(React Higher-order Component)はComponentを引数にして、機能を追加したComponentを返す関数のことです。
helpers内にhocHello.tsというHoCを作成します。
recomposeのlifecycle
を利用することで、classを使用せず、Componentのライフサイクルメソッドを使用できるようになります。
import * as React from 'react'
import { lifecycle } from 'recompose'
export const hocFactory = (component: React.SFC) => {
const wrappedComponent = lifecycle({
componentDidMount() {
console.log('Hello')
},
})(component);
return wrappedComponent
}
components内のhome.tsxをhocFactory
でHoCします。
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
import { hocFactory } from '../helpers/hocHello' // 追加
interface OwnProps {}
const styles = (theme: Theme) => {
return createStyles({
layout: {
marginTop: 100,
padding: theme.spacing.unit * 2,
maxWidth: 1200,
[theme.breakpoints.up(1200)]: {
marginLeft: 'auto',
marginRight: 'auto',
},
},
})
}
type Props = OwnProps & WithStyles<typeof styles>
const HomeComponent: React.SFC<Props> = (props: Props) => {
const { classes } = props
return (
<div>
<AppBar>
<Toolbar>
<Typography color='inherit'>Hello World</Typography>
</Toolbar>
</AppBar>
<div className={classes.layout}>
<Typography>this is home component</Typography>
<Button>Open</Button>
</div>
</div>
)
}
export default withStyles(styles)(hocFactory(HomeComponent)) // 修正
ActionとReducerの作成
typescript-fsaとtypescript-fsa-reducersを利用して、actionとreducerを作成していきます。
Actionについて
actions
内にCardAction.ts
というActionを作成します。
typescript-fsaのactionCreatorFactory
を利用することで、簡潔にFSA(Flux Standard Action)に準じたActionを作成できます。
※FSA(Flux Standard Action)について詳しくはこちら
import actionCreatorFactory from 'typescript-fsa'
const actionCreator = actionCreatorFactory()
export const CardActions = {
open: actionCreator<void>('USER_OPEN_CLICK'),
close: actionCreator<void>('USER_CLOSE_CLICK'),
}
Reducerについて
reducers内にcardReducer.tsというReducerを作成します。
typescript-fsa-reducersのreducerWithInitialState
を利用することで、簡単にtypescript-fsaで作成したActionに対応するReducerを作成できます**。
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import { CardActions } from '../actions/CardAction'
export interface CardState {
open: boolean
}
const initialState: CardState = {
open: false,
}
export const CardReducer = reducerWithInitialState(initialState)
.case(CardActions.open, (state, payload) =>
Object.assign({}, state, { open: true })
)
.case(CardActions.close, (state, payload) =>
Object.assign({}, state, { open: false })
)
Storeの作成と結合
ActionとReducerができたのでStoreを作成します。
src直下にstore.tsというStoreを作成します。
import { createStore, combineReducers } from 'redux'
import { CardReducer, CardState } from './reducers/cardReducer'
export interface AppState {
card: CardState
}
const configureStore = () => {
const _store = createStore(
combineReducers<AppState>({
card: CardReducer,
}),
{},
)
return _store
}
const store = configureStore()
export default store
cardReducerで型を定義したCardState
、を読み込み、AppStateの型定義に利用します。
※actionとreducerが増えれば、AppState
に型を追加していけばよいです。
export interface AppState {
card: CardState
}
configureStore
という関数を作成し、reduxのcreateStore
とcombineReducers
を使用して、_store
を返すように書きます。
※actionとreducerが増えれば、combineReducers
にreducerを追加していけばよいです。
const configureStore = () => {
const _store = createStore(
combineReducers<AppState>({
card: CardReducer,
}),
{},
)
return _store
}
最後に、configureStoreを実行させ、store
をexport default
しておきます。
const store = configureStore()
export default store
次に、作成したstore
を結合します。
すでにあるindex.tsxを編集します。
storeを読み込み、react-reduxのProvider
を使用して結合します。
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux' // 追加
import App from './App'
import './index.css'
import registerServiceWorker from './registerServiceWorker'
import store from './store' // 追加
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
)
registerServiceWorker()
Containerの作成
containers内にhome.tsというContainerを作成します。
import { Action } from 'typescript-fsa'
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { AppState } from '../store'
import { CardActions } from '../actions/CardAction'
import HomeComponent from '../components/home'
export interface HomeActions {
handleOpen: () => Action<void>
handleClose: () => Action<void>
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
handleOpen: () => dispatch(CardActions.open()),
handleClose: () => dispatch(CardActions.close()),
}
}
const mapStateToProps = (appState: AppState) => {
return Object.assign({}, appState)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(HomeComponent)
HomeComponent
でhandleOpen
、handleClose
という関数を扱えるようにするための型定義と、対応するdispatch
をmapDispatchToProps
を書きます。
export interface HomeActions {
handleOpen: () => Action<void>
handleClose: () => Action<void>
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
handleOpen: () => dispatch(CardActions.open()),
handleClose: () => dispatch(CardActions.close()),
}
}
AppState
の値をHomeComponentで読み込めるようにmapStateToProps
を書きます。
const mapStateToProps = (appState: AppState) => {
return Object.assign({}, appState)
}
あとはお決まりのreact-reduxのconnect
でmapDispatchToProps
とmapStateToProps
をHomeComponent
とつなげます。
export default connect(
mapStateToProps,
mapDispatchToProps
)(HomeComponent)
Componentの修正
AppState
とContainerの関数を呼び出せるように、home.tsxを修正します。そして、AppState
によって表示が切り替わるCardを追加します。
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
import Grow from '@material-ui/core/Grow' // 追加
import Card from '@material-ui/core/Card' // 追加
import CardContent from '@material-ui/core/CardContent' // 追加
import CardActions from '@material-ui/core/CardActions' // 追加
import { HomeActions } from '../containers/home' // 追加
import { AppState } from '../store' // 追加
import { hocFactory } from '../helpers/hocHello'
interface OwnProps {}
const styles = (theme: Theme) => {
return createStyles({
layout: {
marginTop: 100,
padding: theme.spacing.unit * 2,
maxWidth: 1200,
[theme.breakpoints.up(1200)]: {
marginLeft: 'auto',
marginRight: 'auto',
},
},
// 追加
card: {
width: 275,
height: 300,
}
})
}
// 修正
type Props = OwnProps & AppState & HomeActions & WithStyles<typeof styles>
const HomeComponent: React.SFC<Props> = (props: Props) => {
const { classes } = props
return (
<div>
<AppBar>
<Toolbar>
<Typography color='inherit'>Hello World</Typography>
</Toolbar>
</AppBar>
<div className={classes.layout}>
<Typography>this is home component</Typography>
<Button onClick={props.handleOpen}>Open</Button> {/* 修正 */}
{/* Card 追加 */}
<Grow in={props.card.open}>
<Card className={classes.card}>
<CardContent>
<Typography>this is card.</Typography>
</CardContent>
<CardActions>
<Button onClick={props.handleClose}>Close</Button>
</CardActions>
</Card>
</Grow>
</div>
</div>
)
}
export default withStyles(styles)(hocFactory(HomeComponent))
Homeの表示
すでにあるApp.tsx
を修正します。
import * as React from 'react'
import Home from './containers/home'
class App extends React.Component {
public render() {
return (
<div className="App">
<Home />
</div>
)
}
}
export default App
npm start
でReactアプリを立ち上げてみましょう。下のような画面が出るはずです。
※typescriptのバリーデーションのエラーで弾かれると思いますが、いわれた通り修正しちゃいましょう。
最後に
typescriptは正直わかりませんが、ComponentのSFC(Stateless Function Component)化、FSA(Flux Standard Action)に準拠したActionは今後当たり前になってくると思いますので(もう当たり前かもしれない)、ぜひ使えるようにしておきましょう。
全体のソースコード公開しておきます。
https://github.com/kousaku-maron/react-sample-for-qiita