概要
React-routerを使って実装しているSPAの中で、唐突に「You cannot change ; it will be ignored」というエラーが出てきたのでその対処をした。
対処法まとめ
自分の場合、原因は
- Routerを扱っているクラス(コンポーネント)でState管理もしていること
であった。そのため
- Routerを扱っているクラスはルーティングのみ行うようにして、State管理はRoutingしている子コンポーネントに任せるようにリファクタする
ことで解消した。
背景
実装しているサイトは自分がReactを学びたての頃に作ったもので、相当基礎がわかっていない状態でできている。現在手直しを入れていて、その過程でReact-routerを導入したりしていたが、全てのコンポーネントが同じファイルに入っていたり別コンポーネントのステートを何故か管理しているコンポーネントがあったりと問題が山積みであった。
結論から言うと本エラーも設計が無茶苦茶だったために起こったものである。
しかしエラー文言からそこに根本原因があるとは想像がつかず、割と解決まで時間を要したためこのように記事にまとめることにする。
実装の概要
class Main extends React.Component {
constructor() {
// 以下関数の中身はここでは割愛する
}
render() {
<MainRoute />
}
}
class MainRoute extends React.Component {
render() {
return (
<Router history={browserHistory} routes={getRoutes()} />
);
}
}
const getRoutes = () => {
const routes = (
<Route path='/' component={Main}>
<IndexRoute component={Top} />
<Route
path='howtouse'
component={HowToUse}
/>
<Route
path='entry'
component={Entry}
/>
<Route
path='hogehoge'
component={Hogehoge}
/>
</Route>
);
return routes;
};
class Main extends React.Component {
render() {
return (
<div className="frame-contents">
<Navigator />
<div className="mod-main">
{this.props.children}
</div>
</div>
);
}
}
関数とかクラスを細かく分けすぎている感じはするけどまあ。。。
React-routerを触ったことがある人なら大体読めると思うけど、作っているサイトには大きく
- Top
- HowToUse
- Entry
- Hogehoge(詳細は割愛)
の4つのページが有り、ページ上部に4ページへのリンクがNavigatorで表示されている格好になっている。
特にエントリーページ(Entry)にはテキスト入力フォームが2つと、ラジオボタン、テキスト入力フォームが1文字以上入力されている場合のみプッシュ可能になるSubmitボタンがあり、内部に状態を保持することが必要となっていた。
ここで、初期開発当時の自分(1年以上前のことである)は下記のようにMain.js
を実装していた。
class Main extends React.Component {
constructor() {
this.setInitialState();
}
setInitialState() {
this.state = {
data: {
name: '',
otherName: '',
place: 1,
},
style: {
},
disabled: {
entryButton: true,
},
};
}
sendData() {
$.ajax({
url: '/user',
type: 'POST',
dataType: 'json',
data: {
name: this.state.data.name,
// (以下APIにユーザー情報をポストする処理)
}
updateFormValue(type, value, event) {
// 渡ってきたtypeとvalueに合わせて画面表示を更新する
switch(type) {
case 'name':
// 省略
}
this.setState({
data: data,
disabled: {
entryButton: data.name.length === 0,
},
});
}
render() {
<MainRoute />
}
}
// (以下省略)
あろうことか、Main.js
にエントリーフォームの情報をStateとして保持する設計にしていたのである。
Main.jsはあくまでエントリーフォームやTopページと言ったページを集約するためにあるのに、そこに子コンポーネントの状態を持っているというのは妙である。
(コード内に出てくる、name、otherName、placeあたりがエントリーフォームから入力される情報)
これらをMain.js
で保持した上で、わざわざEntry.js
にParamsとして渡し、更新時にMain.js
のupdateFormValueを呼び出すような鬼設計にしていた。
const name = {
value: this.state.data.name,
placeholder: "あなたのお名前",
required: true,
type: 'name',
update: this.updateFormValue,
};
// ...
const contentParams = {
params: {
name: name,
},
}
// ...
<MainRoute {...contentParams} />
// ...
class MainRoute extends React.Component {
render() {
return (
<Router history={browserHistory} routes={getRoutes(this.props)} />
);
}
}
// ...
<PropsRoute
path='entry'
component={Entry}
params={props.params}
/>
こんなことをしなくてもEntry.jsの中で全部管理すればいいだけの話なのだが、なぜか開発当初の自分はこのように、Main.js内で各コンポーネントのStateを集約して管理したかったらしい。単なる知識不足だったのですが。
問題
当時はそれでも動いていたのだが、最近僕がReact-routerを導入したことで、Main.jsの役割は単なるルーターになることが求められるようになった。
ルーターかつState集約係という役回りは厳しくなってしまったのだ。
その結果が冒頭のエラー
You cannot change <Router routes>; it will be ignored
である。
つまり、Entry.jsからMain.jsのupdateFormValueを呼ぼうとした時に、Routerの役割を持っているコンポーネントのStateを外部から変更しようとしたためにエラーとなり、Stateの変更がignored
されてしまったのだ。
解決策
GitHubのIssueに同様の事象と、解決策が報告されている。
I found solution for my problem.
I am using shared component for all other components as a parent (it has services and other things in constructor). The problem was, that also containers used this shared component and change of every component in app went directly to router through container.
My "solution" was not to use this shared component for Containers (components defined in router routes). Hope this helps
すべてのコンポーネントの変更がコンテナ経由で直接ルータに送られたことが問題であると述べられており、ルータに共有コンポーネント(つまり、他コンポーネントのStateを持つコンポーネント)を割り当てないことによって解決策とした、と書かれている。
ということで、僕は今までずっとMain.js
に持っていたStateやStateを更新する関数、およびAPIにポストする処理までを全部Entry.jsに丸投げした。
class Entry extends React.Component {
constructor() {
// ...
}
setInitialState() {
this.state = {
data: {
name: '',
pairName: '',
place: 1,
},
style: {
},
disabled: {
entryButton: true,
},
};
}
sendData() {
$.ajax({
url: '/user',
type: 'POST',
dataType: 'json',
data: {
name: this.state.data.name,
// ...
}
updateFormValue(type, value, event) {
// ...
}
render() {
const name = {
// ...
};
return (
<div>
<Title title={'Entry'}/>
<div className="mod-contents">
<div className="mod-form" id="form">
<dl className="formName inputText top">
<dt>
あなたのお名前
</dt>
<dd>
<NameField {...name} />
</dd>
// ...
);
}
}
こうすることによって無事に今まで通り問題なく動いた。
教訓
- React-routerを使った場合、Routerを利用するコンポーネントは他機能や他の状態を一切持たないようにするのが吉
- Reactを使った実装においては、絶えず今扱っている情報があるべきコンポーネント内に収まっているかを見直すほうがいい
- Redux使いたい(諸事情によりそんな時間がなかったので。。。)
以上です。