文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

Typescript, react-router v4, Redux対応時に起きたエラーの対応メモ

色々嵌ってかなりきつかったです。

f:id:treeapps:20190616112954p:plain

会社ではJDK1.6まみれですが、個人開発ではjsにどっぷりになりつつあるtreeです。

tree-mapsでは、babel(ES2017), react, redux, react-router(v3) の組み合わせだったので、今回はちょっと変えて、Typescript(v2.3), react, redux, react-router(v4) の組み合わせを勉強中です。

最初は「Babelと比較してTypescriptの環境構築目茶苦茶楽だわ〜〜」なんて考えてたのですが、Typescriptの書き方が全然解らないし、問題児react-router(v4)のおかがで、動作させるまでに恐ろしく手間がかかったので、色々メモしておこうと思います。

※ 今回はメモ書きレベルなので、精度は低いと思われます。

使用モジュール

package.jsonの抜粋です。

  • "@types/history": "^4.5.1",
  • "@types/react": "^15.0.24",
  • "@types/react-dom": "^15.5.0",
  • "@types/react-router": "^4.0.9",
  • "@types/react-router-dom": "^4.0.4",
  • "create-react-class": "^15.5.3",
  • "prop-types": "^15.5.10",
  • "react": "^15.5.4",
  • "react-dom": "^15.5.4",
  • "react-redux": "^5.0.4",
  • "react-router-dom": "^4.1.1",
  • "react-router-redux": "5.0.0-alpha.4",
  • "redux": "^3.6.0",
  • "redux-logger": "^3.0.1",
  • "typescript": "^2.3.2",
  • "webpack": "^2.5.1",

react-router-reduxが対応していない!?

react-router v4をreduxで管理させようと思ったのですが、2017/05/18現在react-router-reduxのv4系は残念ながらreact-routerのv4系に対応していません。

The next version of react-router-redux will be 5.0.0 and will be compatible with react-router 4.x. It is currently being actively developed over there. Feel free to help out!

https://github.com/reactjs/react-router-redux

なので、5.0.0-alpha.4を使う必要があります。以下のようにインストールします。

yarn add react-router-redux@5.0.0-alpha.4

react-router-reduxとreact-routerを連携

react-router v3系の書き方

ReactDOM.render(
    <Provider store={store}>
        <Router history={history}>
            <Route path='/' component={App}>
                <IndexRoute component={Home}/>
                <Route path='hello' component={Hello}/>
            </Route>
        </Router>
    </Provider>,
    document.getElementById('root')
)

react-router v4系の書き方

大分変わります。エントリポイントにはApp.tsxのみを書きます。

Index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {createBrowserHistory} from 'history'
import {Provider} from 'react-redux'
import {ConnectedRouter} from 'react-router-redux'
import {Route, Redirect} from 'react-router-dom'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import configureStore from './store/configureStore'
import App from './container/App'

const store = configureStore()
const history = createBrowserHistory()

ReactDOM.render(
  <MuiThemeProvider muiTheme={getMuiTheme()}>
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <Route path="/" component={App} />
      </ConnectedRouter>
    </Provider>
  </MuiThemeProvider>,
  document.getElementById('app')
)

materia-uiのMuiThemeProvider -> reduxのProvider -> react-router-reduxのConnectedRouter -> コンテナのApp.tsx という順番にネストしています。

v3の時に書いていたネストしていたIndexRouteやRouteは、App.tsx内に書くようになります。

App.tsx

react-sidebarを使い、左サイドナビのメニューがクリックされたら、右側のコンテナにHomeやHelloコンポーネントを表示する例です。

import * as React from 'react'
import {Route, Redirect, RouteComponentProps} from 'react-router-dom'
import autobind from 'autobind-decorator'
import {AppBar, FlatButton} from 'material-ui'
import Sidebar from 'react-sidebar'
import Leftnavi from '../components/Leftnavi'
import Hello from '../components/Hello'
import Home from '../components/Home'

interface Props extends RouteComponentProps<any> {
  children?: any
}

@autobind
export default class App extends React.Component<Props, any> {

  constructor(props:Props) {
    super(props)
    this.state = {
      sidebarOpen: true
    }
  }

  handledSidebarOpen(open) {
    this.setState({sidebarOpen: open})
  }

  render() {
    return <Sidebar
        sidebar={<Leftnavi {...this.props} />}
        open={false}
        docked={true}
        onSetOpen={this.handledSidebarOpen}
      >
        <div>
          <AppBar
            title='マテリアルUIサンプル'
            iconElementRight={<FlatButton label='AppBar' />}
            style={{marginBottom: '25px'}}
          />

          <div>
             // v3系だとここで {this.props.children} としていました
            <Route exact path="/" component={Home as any} />
            <Route exact path="/hello" component={Hello as any} />
            <Redirect from="*" to="/" />
          </div>
        </div>
      </Sidebar>
  }
}
Leftnavi.tsx
import * as React from 'react'
import * as ReactRouter from 'react-router'
import {PropTypes} from 'prop-types'
import autobind from 'autobind-decorator'
import {List, ListItem} from 'material-ui/List'

interface Props extends ReactRouter.RouteComponentProps<{}> {
  history: PropTypes.historyContext.isRequired,
}

@autobind
export default class Leftnavi extends React.Component<Props, {}> {

  constructor(props:Props) {
    super(props)
  }

  handleChangeMenu(uri) {
    // v3系だと browserHistory.pushだったのが変更されました
    this.props.history.push(uri)
  }

  render() {
    return <div>
      <List>
        <ListItem
          primaryText="Home"
          onTouchTap={this.handleChangeMenu.bind(this, '/')}
        />
        <ListItem
          primaryText="Hello"
          onTouchTap={this.handleChangeMenu.bind(this, '/hello')}
        />
      </List>
    </div>
  }
}

Routeの行で謎のエラーが!?

こんな感じでエラーとなり、以下のようなエラーメッセージが表示されましたf:id:treeapps:20170519014049p:plain

(47,14): error TS2322: Type '{ exact: true; path: "/"; component: typeof Home; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttrib
utes<Route> & Readonly<{ children?: ReactNode; }> & Rea...'.
  Type '{ exact: true; path: "/"; component: typeof Home; }' is not assignable to type 'Readonly<RouteProps>'.
    Types of property 'component' are incompatible.
      Type 'typeof Home' is not assignable to type 'StatelessComponent<RouteComponentProps<any> | undefined> | ComponentClass<RouteComponentProps<any..
.'.
        Type 'typeof Home' is not assignable to type 'ComponentClass<RouteComponentProps<any> | undefined>'.
          Types of parameters 'props' and 'props' are incompatible.
            Type 'RouteComponentProps<any> | undefined' is not assignable to type '{}'.
              Type 'undefined' is not assignable to type '{}'.

これは2パターンの解決方法があるようです。
stackoverflow.com

anyを使う場合

修正前

<Route exact path="/hello" component={Hello} />

修正後

<Route exact path="/hello" component={Hello as any} />
RouteComponentPropsを使う場合
<Route exact path="/hello" component={Hello} />
import {RouteComponentProps} from 'react-router-dom'

interface Props extends RouteComponentProps<any> {
}

export default class Hello extends React.Component<Props, any> {

  constructor(props:Props) {
    super(props)
  }
・・・略・・・

この2パターンの解決から察するに、Routeで指定するcomponentに型を指定しないといけない、という事のようですね。

雑感

最近googleでTypescriptが標準言語として承認されたり、Angular v2系からTypescriptが標準になったりして、世界的にTypescript熱が高まっているようですね。という事で流行りに乗って触ってみたわけですが・・・・もう本当に何が何だか解りません。。。

実際にTypescriptに変えてみて、babelでこう書いていたのをTypescriptで書くにはどうしたらいいのだろう?という疑問が噴出しております。

例えばbabelでReact.Componentと書けていたのに、TypescriptだとReact.Component<Props, any>等と、型指定を強要されます(当たり前ですが)。この時に、どんな型を指定したらいいのかを調べるのに大分苦労しています。

なんか、今のところTypescriptを使ってみて嬉しい点はwebpackの設定がちょっと楽なくらいです。恐らくビジネスロジックを沢山書くようになると型が力を発揮すると思うのですが、現状単にreact + react-router + reduxを連携させるのもヒーヒー言ってるくらいなので、早く型の恩恵を受けるところまでいきたいですね。


今回色々苦労しているのは複合している原因があるからなのですよね。

  • Typescriptを初めて使ってみた。
  • react-routerがv4になって破壊的なバージョンアップが行われた。
  • react-router-reduxがまだreact-router v4に正式対応できていない。
  • react v15.5でReact.PropTypes等が変更された。

これら問題が同時に起きて泣きそうなのに、react + redux + react-touterを連携するために必要なモジュールが沢山あるので、それも嫌な点の1つですね・・・

  • react
  • react-dom
  • react-redux
  • react-router-dom
  • react-router-redux
  • redux

関連モジュールが多い事で、ネット上の情報でどれを見たらいいのか解らない、情報が錯綜しているような事態に見舞われる点がキツイですね。

それぞれのgithubのドキュメントを見ろって話ではありますが、モジュール単独のドキュメントを見ても、各モジュールを全て組み合わせて連携した例は無いですし、やはり定番の組み合わせをした例が欲しいのですが、中々いい記事を見つける事ができず、エラーが出る→StackOverflow先生タスケテー!、という事の繰り返しになってしまいます。

多分、こういう関連モジュールの多さや、それぞれのモジュールが動作するバージョンの違いや、どのドキュメント見たらいいか解らない問題が、ここ数年で言われている「react嫌い」に繋がっているのではないかと思いました。

これはreactに限った話ではないと思いますが、みなさんどうやってこれら問題をスルスルと解決していっているのか、大変気になります。