re-reselect proof of concept + fix Date selector as first example

## Issue
`makeSelectDataForUri` always returns a new reference, so `ClaimPreview` was constantly being rendered. It's pretty expensive since `ClaimPreview`'s rendering checks against a huge blocklist, which is another issue on it's own.

## Changes
- This commit tests the usage of `re-reselect` as the solution to the multi-instance memoization problem (https://github.com/toomuchdesign/re-reselect/blob/master/examples/1-join-selectors.md)
This commit is contained in:
infinite-persistence 2021-10-08 09:57:48 +08:00
parent 9bbd72d179
commit 0c2c21b67e
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
6 changed files with 63 additions and 17 deletions

View file

@ -50,7 +50,6 @@
}, },
"dependencies": { "dependencies": {
"@ungap/from-entries": "^0.2.1", "@ungap/from-entries": "^0.2.1",
"proxy-polyfill": "0.1.6",
"auto-launch": "^5.0.5", "auto-launch": "^5.0.5",
"electron-dl": "^1.11.0", "electron-dl": "^1.11.0",
"electron-log": "^2.2.12", "electron-log": "^2.2.12",
@ -61,6 +60,8 @@
"if-env": "^1.0.4", "if-env": "^1.0.4",
"match-sorter": "^6.3.0", "match-sorter": "^6.3.0",
"parse-duration": "^1.0.0", "parse-duration": "^1.0.0",
"proxy-polyfill": "0.1.6",
"re-reselect": "^4.0.0",
"react-datetime-picker": "^3.2.1", "react-datetime-picker": "^3.2.1",
"react-plastic": "^1.1.1", "react-plastic": "^1.1.1",
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
makeSelectClaimForUri, selectClaimForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
@ -9,7 +9,7 @@ import {
makeSelectClaimWasPurchased, makeSelectClaimWasPurchased,
makeSelectClaimIsStreamPlaceholder, makeSelectClaimIsStreamPlaceholder,
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectDateForUri, selectDateForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import { import {
@ -32,14 +32,14 @@ import ClaimPreview from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
const select = (state, props) => { const select = (state, props) => {
const claim = props.uri && makeSelectClaimForUri(props.uri)(state); const claim = props.uri && selectClaimForUri(state, props.uri);
const media = claim && claim.value && (claim.value.video || claim.value.audio); const media = claim && claim.value && (claim.value.video || claim.value.audio);
const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true }); const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true });
return { return {
claim, claim,
mediaDuration, mediaDuration,
date: props.uri && makeSelectDateForUri(props.uri)(state), date: props.uri && selectDateForUri(state, props.uri),
title: props.uri && makeSelectTitleForUri(props.uri)(state), title: props.uri && makeSelectTitleForUri(props.uri)(state),
pending: props.uri && makeSelectClaimIsPending(props.uri)(state), pending: props.uri && makeSelectClaimIsPending(props.uri)(state),
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state), reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
@ -47,7 +47,6 @@ const select = (state, props) => {
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state), claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state), isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state),
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state), nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state),

View file

@ -7,7 +7,7 @@ import {
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectClaimIsStreamPlaceholder, makeSelectClaimIsStreamPlaceholder,
makeSelectDateForUri, selectDateForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doFileGet } from 'redux/actions/file'; import { doFileGet } from 'redux/actions/file';
import { doResolveUri } from 'redux/actions/claims'; import { doResolveUri } from 'redux/actions/claims';
@ -26,7 +26,7 @@ const select = (state, props) => {
return { return {
claim, claim,
mediaDuration, mediaDuration,
date: props.uri && makeSelectDateForUri(props.uri)(state), date: props.uri && selectDateForUri(state, props.uri),
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state), channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state), thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),

View file

@ -1,11 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectDateForUri } from 'redux/selectors/claims'; import { selectDateForUri } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import DateTime from './view'; import DateTime from './view';
const select = (state, props) => ({ const select = (state, props) => ({
date: props.date || makeSelectDateForUri(props.uri)(state), date: props.date || selectDateForUri(state, props.uri),
clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state), clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state),
}); });
export default connect(select)(DateTime); export default connect(select)(DateTime);

View file

@ -2,20 +2,22 @@
import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI'; import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect';
import { isClaimNsfw, filterClaims } from 'util/claim'; import { isClaimNsfw, filterClaims } from 'util/claim';
import * as CLAIM from 'constants/claim'; import * as CLAIM from 'constants/claim';
type State = { claims: any };
const selectState = (state) => state.claims || {}; const selectState = (state) => state.claims || {};
export const selectById = createSelector(selectState, (state) => state.byId || {}); export const selectById = (state: State) => selectState(state).byId || {};
export const selectPendingClaimsById = (state: State) => selectState(state).pendingById || {};
export const selectPendingClaimsById = createSelector(selectState, (state) => state.pendingById || {});
export const selectClaimsById = createSelector(selectById, selectPendingClaimsById, (byId, pendingById) => { export const selectClaimsById = createSelector(selectById, selectPendingClaimsById, (byId, pendingById) => {
return Object.assign(byId, pendingById); // do I need merged to keep metadata? return Object.assign(byId, pendingById); // do I need merged to keep metadata?
}); });
export const selectClaimIdsByUri = createSelector(selectState, (state) => state.claimsByUri || {}); export const selectClaimIdsByUri = (state: State) => selectState(state).claimsByUri || {};
export const selectCurrentChannelPage = createSelector(selectState, (state) => state.currentChannelPage || 1); export const selectCurrentChannelPage = createSelector(selectState, (state) => state.currentChannelPage || 1);
@ -74,6 +76,43 @@ export const selectReflectingById = createSelector(selectState, (state) => state
export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]); export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]);
export const selectClaimForUri = createCachedSelector(
selectClaimIdsByUri,
selectClaimsById,
(state, uri) => uri,
(state, uri, returnRepost = true) => returnRepost,
(byUri, byId, uri, returnRepost) => {
const validUri = isURIValid(uri);
if (validUri && byUri) {
const claimId = uri && byUri[normalizeURI(uri)];
const claim = byId[claimId];
// Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined)
if (claimId === null) {
return null;
} else if (claimId === undefined) {
return undefined;
}
const repostedClaim = claim && claim.reposted_claim;
if (repostedClaim && returnRepost) {
const channelUrl =
claim.signing_channel && (claim.signing_channel.canonical_url || claim.signing_channel.permanent_url);
return {
...repostedClaim,
repost_url: normalizeURI(uri),
repost_channel_url: channelUrl,
repost_bid_amount: claim && claim.meta && claim.meta.effective_amount,
};
} else {
return claim;
}
}
}
)((state, uri, returnRepost = true) => `${uri}:${returnRepost ? '1' : '0'}`);
export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true) => export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true) =>
createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => { createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => {
const validUri = isURIValid(uri); const validUri = isURIValid(uri);
@ -245,8 +284,9 @@ export const makeSelectMetadataItemForUri = (uri: string, key: string) =>
export const makeSelectTitleForUri = (uri: string) => export const makeSelectTitleForUri = (uri: string) =>
createSelector(makeSelectMetadataForUri(uri), (metadata) => metadata && metadata.title); createSelector(makeSelectMetadataForUri(uri), (metadata) => metadata && metadata.title);
export const makeSelectDateForUri = (uri: string) => export const selectDateForUri = createCachedSelector(
createSelector(makeSelectClaimForUri(uri), (claim) => { selectClaimForUri, // input: (state, uri, ?returnRepost)
(claim) => {
const timestamp = const timestamp =
claim && claim &&
claim.value && claim.value &&
@ -260,7 +300,8 @@ export const makeSelectDateForUri = (uri: string) =>
} }
const dateObj = new Date(timestamp); const dateObj = new Date(timestamp);
return dateObj; return dateObj;
}); }
)((state, uri) => uri);
export const makeSelectAmountForUri = (uri: string) => export const makeSelectAmountForUri = (uri: string) =>
createSelector(makeSelectClaimForUri(uri), (claim) => { createSelector(makeSelectClaimForUri(uri), (claim) => {

View file

@ -13317,6 +13317,11 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
re-reselect@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-4.0.0.tgz#9ddec4c72c4d952f68caa5aa4b76a9ed38b75cac"
integrity sha512-wuygyq8TXUlSdVXv2kigXxQNOgdb9m7LbIjwfTNGSpaY1riLd5e+VeQjlQMyUtrk0oiyhi1AqIVynworl3qxHA==
react-async-script@^1.1.1: react-async-script@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf" resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf"