-
Notifications
You must be signed in to change notification settings - Fork 674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Return previous state if the props change but not the output #441
Comments
I believe you can do this with createSelectorCreator. |
I believe this would accomplish what OP wants, albeit in a more verbose way. It might be helpful to provide an API surface like the one OP was asking for as it would allow the consumer to more easily choose when to cache the result or not. import { createSelectorCreator } from 'reselect';
import { shallowEqualArrays } from 'shallow-equal';
// START STUFF TAKEN DIRECTLY FROM RESELECT SOURCE
function defaultEqualityCheck(a, b) {
return a === b;
}
function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
if (prev === null || next === null || prev.length !== next.length) {
return false;
}
// Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
const length = prev.length;
for (let i = 0; i < length; i++) {
if (!equalityCheck(prev[i], next[i])) {
return false;
}
}
return true;
}
// END STUFF TAKEN DIRECTLY FROM RESELECT SOURCE
function customMemoize(func, resultEqualityCheck = defaultEqualityCheck, equalityCheck = defaultEqualityCheck) {
let lastArgs = null;
let lastResult = null;
// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
// lastResult = func.apply(null, arguments)
// BEGIN CUSTOMIZATION OF MEMOIZE FUNCTION TO ALSO MEMOIZE RESULT
const result = func.apply(null, arguments);
if (!resultEqualityCheck(lastResult, result)) {
lastResult = result;
}
// END CUSTOMIZATION OF MEMOIZE FUNCTION TO ALSO MEMOIZE RESULT
}
lastArgs = arguments;
return lastResult;
}
}
const createArrayResultSelector = createSelectorCreator(customMemoize, shallowEqualArrays);
const currentUsersPosts = createArrayResultSelector(
[state => state.currentUser.id, state=> state.posts],
(userId, posts) => posts.filter(p=>p.id === userId)
); |
FWIW heres how I solved it myself import { createSelectorCreator } from 'reselect';
import { shallowEqualArrays, shallowEqualObjects } from 'shallow-equal';
const resultMemoize = (func) => {
let lastArgs = null;
let lastResult = null;
return (...args) => {
if (!shallowEqualArrays(lastArgs, args)) {
// faster with apply, this might get called a lot
// https://medium.com/@pouyae/what-is-the-es6-spread-operator-and-why-you-shouldnt-use-it-57c056078ed9
// eslint-disable-next-line prefer-spread
const nextResult = func.apply(null, args);
if (Array.isArray(nextResult) && shallowEqualArrays(nextResult, lastResult)) {
// keep the last result
} else if (typeof nextResult === 'object' && shallowEqualObjects(nextResult, lastResult)) {
// keep the last result
} else {
lastResult = nextResult;
}
}
lastArgs = args;
return lastResult;
};
};
export default createSelectorCreator(resultMemoize); usage: export const selector = createResultCheckingSelector(
[
state => state.complexThings
],
complexThings => complexThings.reduce((acc, thingKey) => {
acc[thingKey] =state.complexThings[thingKey].simpleThing;
return acc;
}, {})
); If other unrelated parts of my state are mutated it wont trigger everything else to re-render. The bit in here thats gross is that I have to keep my own copy of args and lastResult (which reselect also keeps but doesn't expose). |
You can manually expose the last result of the selector yourself. import { createSelector } from 'reselect';
import { shallowEqualArrays } from 'shallow-equal';
const currentUsersPosts = createSelector(
[state => state.currentUser.id, state=> state.posts],
(userId, posts) => {
const lastResult = currentUsersPosts.lastResult;
const result = posts.filter(p=>p.id === userId);
if (shallowEqualArrays(lastResult, result)) return lastResult;
currentUsersPosts.lastResult = result;
return result;
}
); |
interesting, ill give that a go, it will clean things up quite a bit. |
Not sure why nobody uses defaultMemoize that's already in reselect to do this: import { defaultMemoize, createSelector } from 'reselect';
const createMemoizeArray = () => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
const selectCurrentUsersPosts = ((memoizeArry) =>
createSelector(
[
(state) => state.currentUser.id,
(state) => state.posts,
],
(userId, posts) =>
memoizeArry(posts.filter((p) => p.id === userId))
))(createMemoizeArray());//IIFE When you want to pass user id to the selector because you you want to show list of users and posts of each user you can do the following: const createMemoizeArray = () => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
const createSelectCurrentUsersPosts = (userId) => {//curry user id
const memoizeArry = createMemoizeArray();//create its own memoize array
return createSelector([(state) => state.posts], (posts) =>
memoizeArry(posts.filter((p) => p.id === userId))
);
};
// used in components
const UserList = (users) => (
<ul>
{users.map((user) => (
<UserContainer user={user} key={user.id} />
))}
</ul>
);
const UserContainer = ({ user }) => {
const selectUserPosts = React.useMemo(//(re) create selector based on user id
createSelectCurrentUsersPosts(user.id),
[user.id]
);
const userPosts = useSelector(selectUserPosts);
return 'jsx';
}; |
@amsterdamharu just wanted to extend your brilliant answer with a const memoizeArrayProducingFn = (fn) => {
const memArray = defaultMemoize((...array) => array);
return (...args) => memArray(...fn(...args));
};
const selectCurrentUsersPosts createSelector(
[
(state) => state.currentUser.id,
(state) => state.posts,
],
memoizeArrayProducingFn((userId, posts) => posts.filter((p) => p.id === userId))
); |
@akmjenkins thanks for the nice succinct function above. For those using typescript, I found that the function above destroys the types associated with the output arguments of
|
Thanks @akmjenkins for the short and sweet solution and thanks @Mchristos for the typed version.
|
Oh hey, those are pretty neat! Seems like a common enough problem that it might even be worth at least including this as a documented recipe, if not an actual API addition. |
I've been using a similar idea which I've called "stabilize". (A "stable" identity is one that doesn't needlessly change.)
These helpers can be used to memoize not just selectors which return arrays, but selectors which return any non-primitive type. Furthermore, this idea can be used as a React hook called import { shallowEqualArrays } from 'shallow-equal';
//
// Example usage in Reselect
//
import assert from 'assert';
import { createSelector, defaultMemoize } from 'reselect';
const stabilize = <A extends unknown[], R>(
fn: (...args: A) => R,
isResultEqual: (a: R, b: R) => boolean,
): ((...args: A) => R) => {
const memoizedIdentity = defaultMemoize(
(result: R) => result,
// `defaultMemoize` has bad typings.
// @ts-ignore
isResultEqual,
);
return (...args) => memoizedIdentity(fn(...args));
};
/** Re-use the previous result if the Array value matches the latest result */
const stabilizeArray = <A extends unknown[], R>(
fn: (...args: A) => Array<R>,
): ((...args: A) => Array<R>) => stabilize(fn, shallowEqualArrays);
type User = {
id: string;
age?: number;
};
type State = {
users: { [key: string]: User };
};
const state1: State = {
users: {
foo: { id: 'foo' },
bar: { id: 'bar' },
},
};
const state2: State = {
users: {
foo: { id: 'foo', age: 1 },
bar: { id: 'bar' },
},
};
const getUsersById = (state: State) => state.users;
const getUserIds = stabilizeArray(
createSelector(getUsersById, (users) => Object.keys(users)),
);
// OR
// const getUserIds = createSelector(
// getUsersById,
// stabilizeArray((users) => Object.keys(users)),
// );
assert(getUserIds(state1) === getUserIds(state2));
//
// Example usage in React
//
import React, { FC, useMemo, useRef } from 'react';
/**
* This is a hook version of `stabilize`.
* Only store/return a new value when it's determined to be inequal to the previous value.
*/
const useStable = <T,>(value: T, equalityCheck: (a: T, b: T) => boolean): T => {
const ref = useRef(value);
if (equalityCheck(ref.current, value) === false) {
ref.current = value;
return value;
} else {
return ref.current;
}
};
// `userIds` has same reference identity on second render
const MyComponent: FC<{ users: State['users'] }> = ({ users }) => {
const _userIds = useMemo(() => Object.keys(users), [users]);
const userIds = useStable(_userIds, shallowEqualArrays);
console.log('render', userIds);
return null;
};
<MyComponent users={state1.users} />;
<MyComponent users={state2.users} />; |
Resolved by #513 . |
I have an array of posts, which is filtered by user such that I have the following
When a new post is creates for another user this correctly filters the list and produces an identical (but differently referenced) array. This means that I re-render the component needlessly. Is there a way to get at the previously memo'd value for this instance of the reselector and return that if it's shallow equal (hence avoiding children re-rendering)?
eg:
The text was updated successfully, but these errors were encountered: