From 0736723200edbb8ff050921140b5c7d2788ee816 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Mon, 8 Nov 2021 16:30:48 +0800 Subject: [PATCH] Don't update 'byId' if no changes + add 'selectClaimWithId' ## Ticket 116 Claim store optimization ideas (reducing unnecessary renders) ## Changes - Ignore things like `confirmations` so that already-fetched claims aren't invalidated and causes re-rendering. The `stringify` might look expensive, but the amount of avoided re-renders outweighs it. There might be faster ways to compare, though. - With `byId[claimId]` references more stable now, memoized selectors can now use 'selectClaimWithId' to pick a specific claim to depend on, instead of 'byId' which changes on every update. --- ui/redux/reducers/claims.js | 49 ++++++++++++++++++++++++++++++------ ui/redux/selectors/claims.js | 15 +++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ui/redux/reducers/claims.js b/ui/redux/reducers/claims.js index 6b62b53bd..af6a5032d 100644 --- a/ui/redux/reducers/claims.js +++ b/ui/redux/reducers/claims.js @@ -130,6 +130,35 @@ function resolveDelta(original: any, delta: any) { } } +function claimObjHasNewData(original, fresh) { + // Don't blow away 'is_my_output' just because the next query didn't ask for it. + const ignoreIsMyOutput = original.is_my_output !== undefined && fresh.is_my_output === undefined; + + // Something is causing the tags to be re-ordered differently + // (https://github.com/OdyseeTeam/odysee-frontend/issues/116#issuecomment-962747147). + // Just do a length comparison for now, which covers 99% of cases while we + // figure out what's causing the order to change. + const ignoreTags = + original.value && + fresh.value && + original.value.tags && + fresh.value.tags && + original.value.tags.length !== fresh.value.tags.length; + + const excludeKeys = (key, value) => { + if (key === 'confirmations' || (ignoreTags && key === 'tags') || (ignoreIsMyOutput && key === 'is_my_output')) { + return undefined; + } + + return value; + }; + + const originalStringified = JSON.stringify(original, excludeKeys); + const freshStringified = JSON.stringify(fresh, excludeKeys); + + return originalStringified !== freshStringified; +} + // **************************************************************************** // handleClaimAction // **************************************************************************** @@ -138,7 +167,7 @@ function handleClaimAction(state: State, action: any): State { const { resolveInfo }: ClaimActionResolveInfo = action.data; const byUri = Object.assign({}, state.claimsByUri); - const byId = Object.assign({}, state.byId); + const byIdDelta = {}; const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const pendingById = state.pendingById; let newResolvingUrls = new Set(state.resolvingUris); @@ -151,10 +180,13 @@ function handleClaimAction(state: State, action: any): State { if (stream) { if (pendingById[stream.claim_id]) { - byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]); + byIdDelta[stream.claim_id] = mergeClaim(stream, state.byId[stream.claim_id]); } else { - byId[stream.claim_id] = stream; + if (!state.byId[stream.claim_id] || claimObjHasNewData(state.byId[stream.claim_id], stream)) { + byIdDelta[stream.claim_id] = stream; + } } + byUri[url] = stream.claim_id; // If url isn't a canonical_url, make sure that is added too @@ -181,9 +213,9 @@ function handleClaimAction(state: State, action: any): State { } if (pendingById[channel.claim_id]) { - byId[channel.claim_id] = mergeClaim(channel, byId[channel.claim_id]); + byIdDelta[channel.claim_id] = mergeClaim(channel, state.byId[channel.claim_id]); } else { - byId[channel.claim_id] = channel; + byIdDelta[channel.claim_id] = channel; } byUri[channel.permanent_url] = channel.claim_id; @@ -194,10 +226,11 @@ function handleClaimAction(state: State, action: any): State { if (collection) { if (pendingById[collection.claim_id]) { - byId[collection.claim_id] = mergeClaim(collection, byId[collection.claim_id]); + byIdDelta[collection.claim_id] = mergeClaim(collection, state.byId[collection.claim_id]); } else { - byId[collection.claim_id] = collection; + byIdDelta[collection.claim_id] = collection; } + byUri[url] = collection.claim_id; byUri[collection.canonical_url] = collection.claim_id; byUri[collection.permanent_url] = collection.claim_id; @@ -216,7 +249,7 @@ function handleClaimAction(state: State, action: any): State { }); return Object.assign({}, state, { - byId, + byId: resolveDelta(state.byId, byIdDelta), claimsByUri: byUri, channelClaimCounts, resolvingUris: Array.from(newResolvingUrls), diff --git a/ui/redux/selectors/claims.js b/ui/redux/selectors/claims.js index 326e221a8..3c2a0e427 100644 --- a/ui/redux/selectors/claims.js +++ b/ui/redux/selectors/claims.js @@ -43,6 +43,21 @@ export const selectClaimsByUri = createSelector(selectClaimIdsByUri, selectClaim return claims; }); +/** + * Returns the claim with the specified ID. The claim could be undefined if does + * not exist or have not fetched. Take note of the second parameter, which means + * an inline function or helper would be required when used as an input to + * 'createSelector'. + * + * @param state + * @param claimId + * @returns {*} + */ +export const selectClaimWithId = (state: State, claimId: string) => { + const byId = selectClaimsById(state); + return byId[claimId]; +}; + export const selectAllClaimsByChannel = createSelector(selectState, (state) => state.paginatedClaimsByChannel || {}); export const selectPendingIds = createSelector(selectState, (state) => Object.keys(state.pendingById) || []);