問題
react-router と react-redux を使っているときにありがちな,「URLは変わるんだけどコンポーネントが再描画されない」 というトラブル。
ReactDOM.render(
<Provider store={store}>
<Router>
<Switch>
<Route component={Timeline} path="/articles" />
</Switch>
</Router>
</Provider>,
document.getElementById('root'),
)
const Timeline = ({ articles }) => (
<div>
<ul>
{articles.map(({ title, body }) => (
<li key={article.id}>
<h1>{title}</h1>
<p>{body}</p>
</li>
))}
</ul>
<Route component={ArticleDetail} path={'/articles/:articleId'} />
/* ↑ IDが指定されたときだけタイムラインの上に特定記事の詳細画面をオーバーレイ */
</div>
)
export default connect(
({ articles }) => ({ articles: articles.data }),
)(Timeline)
const ArticleDetail = ({ title, body, ...otherProps }) => (
<div>
<h1>{title}</h1>
<p>{body}</p>
<Link to='/articles/:articleId/favorites'>いいね一覧</Link>
/*
いいね一覧表示ロジックを書く (省略)
*/
</div>
)
export default connect(
({ articles }, { match: { params } }) => selectArticleById(articles.data, params.articleId),
)(ArticleDetail)
- 親
<Route />
で指定しているコンポーネントの中に更に子<Route />
がある - 子
<Route />
もconnect()
でラップされている
こういう状況のとき,例えば「いいね一覧」をクリックして Timeline
には一切影響を与えずに ArticleDetail
内のみの再描画を行わせる という処理を行おうとしても,一向に再描画が行われなくなります。
原因
react-redux の connect()
の最適化が悪さをしています。
shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}
この this.selector.shouldComponentUpdate
の算出にはプロパティの比較結果が使われます。
const hasOwn = Object.prototype.hasOwnProperty
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
return true
}
コンポーネントに渡ってくるプロパティが shallowEquals
関数を使って比較され,等しいと判断された場合には再描画が行われなくなります。
- 子はURLの変更で受け取るプロパティが変化するので再描画されるべき
- 親はURLが変更されても受け取るプロパティが変化しないので再描画されない
- 結果,子も再描画されない
という流れです。
対策
connect()
したコンポーネントの中で connect()
しない
理想。ですが依存ライブラリも考慮した設計上 connect()
せざるを得ない場所もあるので,銀の弾丸とはなりません。
location
を受け取る
公式で紹介されている対処法です。 <Route />
は match
location
history
を描画するコンポーネントに渡しますが,この location
を受け取ればURLの変更を検知できるようになります。
const Timeline = ({ articles }) => (
<div>
<ul>
{articles.map(({ title, body }) => (
<li key={article.id}>
<h1>{title}</h1>
<p>{body}</p>
</li>
))}
</ul>
<Route component={ArticleDetail} path={'/articles/:articleId'} />
/* ↑ IDが指定されたときだけタイムラインの上に特定記事の詳細画面をオーバーレイ */
</div>
)
export default connect(
- ({ articles }) => ({ articles: articles.data }),
+ ({ articles }, { location }) => ({ articles: articles.data, location }),
)(Timeline)
たまたまこの Timeline
は直属の親コンポーネントが <Route />
なので問題ありませんが,もしそうではない場合, withRouter()
でラップする必要があります。この関数は <Route />
で引数をラップすることをやってくれるだけのシンプルな実装です。
const Timeline = ({ articles }) => (
<div>
<ul>
{articles.map(({ title, body }) => (
<li key={article.id}>
<h1>{title}</h1>
<p>{body}</p>
</li>
))}
</ul>
<Route component={ArticleDetail} path={'/articles/:articleId'} />
/* ↑ IDが指定されたときだけタイムラインの上に特定記事の詳細画面をオーバーレイ */
</div>
)
- export default connect(
- ({ articles }) => ({ articles: articles.data }),
- )(Timeline)
+ export default withRouter(connect(
+ ({ articles }, { location }) => ({ articles: articles.data, location }),
+ )(Timeline))
react-router-redux を使う場合はストアから直接 location
を受け取ることができるので,無駄なHOCが削減できます。
ReactDOM.render(
<Provider store={store}>
- <Router>
+ <ConnectedRouter history={history}>
<Switch>
<Route component={Timeline} path="/articles" />
</Switch>
- </Router>
+ </ConnectedRouter>
</Provider>,
document.getElementById('root')
)
const Timeline = ({ articles }) => (
<div>
<ul>
{articles.map(({ title, body }) => (
<li key={article.id}>
<h1>{title}</h1>
<p>{body}</p>
</li>
))}
</ul>
<Route component={ArticleDetail} path={'/articles/:articleId'} />
/* ↑ IDが指定されたときだけタイムラインの上に特定記事の詳細画面をオーバーレイ */
</div>
)
export default connect(
- ({ articles }) => ({ articles: articles.data }),
+ ({ articles, routing: { location } }) => ({ articles: articles.data, location }),
)(Timeline)
参考
react-router/blocked-updates.md at 76e7c81e8605f267dd26833e23a2d24552d24110 · ReactTraining/react-router
unimplement shouldComponentUpdate? · Issue #507 · reactjs/react-redux