サンプルコード: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js
ライブデモを見る: http://d6u.github.io/example-redux-update-nested-props/one-connect.html
上記のコンポーネント、RepoおよびRepoListがあります。最初のレポのタグを更新したい( Line 14 )。そこで、UPDATE_TAG
アクション。 shouldComponentUpdate
を実装する前に、ディスパッチには約200ミリ秒かかります。これは、<Repo/>
sは変更されていません。
shouldComponentUpdate
を追加した後、ディスパッチには約30ミリ秒かかります。実稼働ビルドReact.jsの後、更新のコストは約17ミリ秒です。これははるかに優れていますが、Chrome devコンソールのタイムラインビューはまだジャンクフレーム(16.6msより長い)を示しています。
このような多くの更新がある場合、または<Repo/>
は現在のものよりも複雑で、60fpsを維持できません。
私の質問は、ネストされたコンポーネントの小道具に対するこのような小さな更新について、コンテンツを更新するより効率的で標準的な方法はありますか? Reduxを引き続き使用できますか?
すべてのtags
を観察可能な内部レデューサーに置き換えることで解決策を得ました。何かのようなもの
// inside reducer when handling UPDATE_TAG action
// repos[0].tags of state is already replaced with a Rx.BehaviorSubject
get('repos[0].tags', state).onNext([{
id: 213,
text: 'Node.js'
}]);
次に、 https://github.com/jayphelps/react-observable-subscribe を使用して、Repoコンポーネント内の値にサブスクライブします。これはうまくいきました。 React.jsの開発ビルドでも、すべてのディスパッチの費用はわずか5ミリ秒です。しかし、これはReduxのアンチパターンのように感じます。
私はダン・アブラモフの回答の推奨事項に従い、 私の状態を正規化 および 更新された接続コンポーネント
新しい状態の形状は次のとおりです。
{
repoIds: ['1', '2', '3', ...],
reposById: {
'1': {...},
'2': {...}
}
}
追加した console.time
周りReactDOM.render
to time 初期レンダリング 。
ただし、パフォーマンスは以前よりも低下します(最初のレンダリングと更新の両方)。 (ソース: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js 、ライブデモ: http ://d6u.github.io/example-redux-update-nested-props/repo-connect.html )
// With dev build
INITIAL: 520.208ms
DISPATCH: 40.782ms
// With prod build
INITIAL: 138.872ms
DISPATCH: 23.054ms
<Repo/>
には多くのオーバーヘッドがあります。
Danの更新された回答に基づいて、代わりにconnect
のmapStateToProps
引数を返す必要があります。ダンの答えをご覧ください。 デモ も更新しました。
以下では、私のコンピューターのパフォーマンスがはるかに優れています。そして、単に楽しみのために、私が話したレデューサーアプローチに副作用も追加しました( source 、 demo )(それは、実験専用です)。
// in prod build (not average, very small sample)
// one connect at root
INITIAL: 83.789ms
DISPATCH: 17.332ms
// connect at every <Repo/>
INITIAL: 126.557ms
DISPATCH: 22.573ms
// connect at every <Repo/> with memorization
INITIAL: 125.115ms
DISPATCH: 9.784ms
// observables + side effect in reducers (don't use!)
INITIAL: 163.923ms
DISPATCH: 4.383ms
react-virtualized example を追加しました。
INITIAL: 31.878ms
DISPATCH: 4.549ms
const App = connect((state) => state)(RepoList)
がどこから来たのかわかりません。
React Redux docsの対応する例には通知があります :
これをしないでください! TodoAppはすべてのアクションの後に再レンダリングするため、パフォーマンスの最適化はすべて終了します。ビュー階層内のいくつかのコンポーネントでより詳細なconnect()を使用し、各コンポーネントが状態の関連するスライスのみをリッスンすることをお勧めします。
このパターンを使用することはお勧めしません。むしろ、それぞれが<Repo>
に接続するため、mapStateToProps
内の独自のデータを読み取ります。 「 tree-view 」の例は、その方法を示しています。
状態の形状をさらに 正規化 (現在はすべてネストされている)にすると、repoIds
をreposById
から分離し、RepoList
のみを再作成できます。 repoIds
が変更された場合にレンダリングします。この方法で個々のリポジトリに変更してもリスト自体には影響せず、対応するRepo
のみが再レンダリングされます。 このプルリクエスト は、それがどのように機能するかを示しているかもしれません。 「 real-world 」の例は、正規化されたデータを処理するレデューサーの作成方法を示しています。
ツリーを正規化することで提供されるパフォーマンスを実際に活用するには、 this pull request のように実行し、mapStateToProps()
ファクトリーをconnect()
に渡す必要があります。
const makeMapStateToProps = (initialState, initialOwnProps) => {
const { id } = initialOwnProps
const mapStateToProps = (state) => {
const { todos } = state
const todo = todos.byId[id]
return {
todo
}
}
return mapStateToProps
}
export default connect(
makeMapStateToProps
)(TodoItem)
これが重要な理由は、IDが決して変わらないことを知っているからです。 ownProps
を使用するとパフォーマンスが低下します。外側のプロップが変更されるたびに、内側のプロップを再計算する必要があります。ただし、initialOwnProps
を使用しても1回しか使用されないため、このペナルティは発生しません。
サンプルの高速バージョンは次のようになります。
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider, connect} from 'react-redux';
import set from 'lodash/fp/set';
import pipe from 'lodash/fp/pipe';
import groupBy from 'lodash/fp/groupBy';
import mapValues from 'lodash/fp/mapValues';
const UPDATE_TAG = 'UPDATE_TAG';
const reposById = pipe(
groupBy('id'),
mapValues(repos => repos[0])
)(require('json!../repos.json'));
const repoIds = Object.keys(reposById);
const store = createStore((state = {repoIds, reposById}, action) => {
switch (action.type) {
case UPDATE_TAG:
return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);
default:
return state;
}
});
const Repo = ({repo}) => {
const [authorName, repoName] = repo.full_name.split('/');
return (
<li className="repo-item">
<div className="repo-full-name">
<span className="repo-name">{repoName}</span>
<span className="repo-author-name"> / {authorName}</span>
</div>
<ol className="repo-tags">
{repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)}
</ol>
<div className="repo-desc">{repo.description}</div>
</li>
);
}
const ConnectedRepo = connect(
(initialState, initialOwnProps) => (state) => ({
repo: state.reposById[initialOwnProps.repoId]
})
)(Repo);
const RepoList = ({repoIds}) => {
return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>;
};
const App = connect(
(state) => ({repoIds: state.repoIds})
)(RepoList);
console.time('INITIAL');
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('app')
);
console.timeEnd('INITIAL');
setTimeout(() => {
console.time('DISPATCH');
store.dispatch({
type: UPDATE_TAG
});
console.timeEnd('DISPATCH');
}, 1000);
ConnectedRepo
ではなくinitialOwnProps
でファクトリを使用するようにownProps
のconnect()
を変更したことに注意してください。これにより、React Reduxはすべてのプロップの再評価をスキップします。
React ReduxがshouldComponentUpdate()
での実装を処理するため、<Repo>
上の不要なconnect()
も削除しました。
このアプローチは、私のテストで以前の両方のアプローチを打ち負かしています:
one-connect.js: 43.272ms
repo-connect.js before changes: 61.781ms
repo-connect.js after changes: 19.954ms
最後に、このような大量のデータを表示する必要がある場合は、とにかく画面に収まりません。この場合、 仮想化テーブル を使用して、実際に表示するパフォーマンスのオーバーヘッドなしに数千行をレンダリングできるようにすることをお勧めします。
すべてのタグを観察可能な内部レデューサーに置き換えることで解決策を得ました。
副作用がある場合は、Reduxレデューサーではありません。うまくいくかもしれませんが、混乱を避けるためにこのようなコードをReduxの外部に置くことをお勧めします。 Reduxレデューサーは純粋な関数である必要があり、サブジェクトでonNext
を呼び出すことはできません。