firefox-devtools / profiler

Firefox Profiler — Web app for Firefox performance analysis
https://profiler.firefox.com
Mozilla Public License 2.0
1.14k stars 372 forks source link

Update reselect 4.1.8 → 5.1.1 (major) #5023

Closed depfu[bot] closed 2 weeks ago

depfu[bot] commented 2 weeks ago

Here is everything you need to know about this upgrade. Please take a good look at what changed and the test results before merging this pull request.

What changed?

✳️ reselect (4.1.8 → 5.1.1) · Repo · Changelog

Release Notes

5.1.1

This patch release fixes behavior of resultEqualityCheck in weakMapMemoize, fixes the case of lruMemoize being given a maxSize less than 1, and tweaks the internal implementation of lruMemoize. (We've also updated our general build tooling.)

Changelog

Bug fixes

Previously, providing the resultEqualityCheck option to weakMapMemoize resulted in it being called with empty objects as part of the initialization / dev check process. That could be an issue if your comparison function expected different values. We've updated the logic to avoid that, as well as improving a couple other perf aspects.

Previously, passing a maxSize < 1 to lruMemoize would result in it creating a larger cache. That's now fixed.

lruMemoize now uses a symbol for its NOT_FOUND value instead of a string.

What's Changed

Full Changelog: v5.1.0...v5.1.1

5.1.0

This minor release:

  • Adds a new createSelector.withTypes<RootState>() and createStructuredSelector.withTypes<RootState>() API
  • Deprecates the TypedStructuredSelectorCreator type introduced in 5.0
  • Aims to reduce false positives in identityFunctionCheck by only running if the output selector is passed one argument
  • Fixes a bug with weakMapMemoize's resultEqualityCheck when used with a primitive result.

withTypes

Most commonly, selectors will accept the root state of a Redux store as their first argument. withTypes allows you to specify what that first argument will be ahead of creating the selector, meaning it doesn't have to be specified.

// previously
export const selectPostById = createSelector(
  [
    (state: RootState) => state.posts.entities,
    (state: RootState, id: number) => id,
  ],
  (entities, id) => entities[id],
);
// now
export const createAppSelector = createSelector.withTypes<RootState>();

export const selectPostById = createAppSelector(
  [(state) => state.posts.entities, (state, id: number) => id],
  (entities, id) => entities[id],
);

Known limitations

Due to a Typescript issue, inference of the output selector's parameters only works with withTypes when using an array of input selectors.

If using the variadic version, you can either wrap your input selectors in an array instance (as above), or annotate the parameters manually.

export const createAppSelector = createSelector.withTypes<RootState>();

export const selectPostById = createAppSelector(
  (state) => state.posts.entities, 
  (state, id: number) => id,
  // parameters cannot be inferred, so need annotating
  (entities: Record<number, Post>, id: number) => entities[id],
);

What's Changed

New Contributors

Full Changelog: v5.0.1...v5.1.0

5.0.1

This major release:

  • Switches createSelector to use a new weakMapMemoize method as the default memoizer
  • Renames the existing defaultMemoize method to lruMemoize
  • Adds new configuration options to createSelector
  • Adds new development mode checks
  • Updates the packaging for better ESM/CJS compatibility and modernizes the build output
  • Makes significant updates to the TS types.

This release has breaking changes. (note: this points to v5.0.1, which contains a hotfix that was released prior to the announcement.)

This release is part of a wave of major versions of all the Redux packages: Redux Toolkit 2.0, Redux core 5.0, React-Redux 9.0, Reselect 5.0, and Redux Thunk 3.0.

For full details on all of the breaking changes and other significant changes to all of those packages, see the "Migrating to RTK 2.0 and Redux 5.0" migration guide in the Redux docs.

We have a new docs site! The Reselect docs are now at https://reselect.js.org.

Note

The Redux core, Reselect, and Redux Thunk packages are included as part of Redux Toolkit, and RTK users do not need to manually upgrade them - you'll get them as part of the upgrade to RTK 2.0. (If you're not using Redux Toolkit yet, please start migrating your existing legacy Redux code to use Redux Toolkit today!)

# RTK
npm install @reduxjs/toolkit
yarn add @reduxjs/toolkit

# Standalone
npm install reselect
yarn add reselect

Changelog

createSelector Uses weakMapMemoize By Default

Reselect's createSelector originally only had one memoization function, which has originally called defaultMemoize (and per below, is now renamed to lruMemoize). It's always used a customizable comparison method to compare each argument. Over time, we added more functionality, particularly in v4.1.0 where lruMemoize gained options for {memoize, maxSize, resultEqualityCheck}.

However, lruMemoize has limitations. The biggest one is that the default cache size is 1. This makes selector instances hard to reuse in scenarios like list items, which might call selectSomeValue(state, props.id), and thus never actually memoize due to changing arguments. There are workarounds, but they're cumbersome - using createSelectorCreator to create a customized createSelector function with a different memoization implementation, creating unique selector instances per component, or setting a fixed maxSize.

For 5.0, we added a new weakMapMemoize memoization function, which takes a different approach (as originally implemented in the React codebase). It uses an internal tree of cache nodes rather than a single value or a list of values. This gives weakMapMemoize an effectively infinite cache size!

We've done a fair amount of testing, and weakMapMemoize both performs faster and has more frequent cache hits than lruMemoize.

Given that, we've made the switch so that createSelector uses weakMapMemoize by default! This should result in better performance for Redux and React apps that use Reselect.

This is hopefully a mostly non-breaking change at the code level, and an overall improvement at the behavior level.

This is a breaking change. weakMapMemoize does not have an equalityCheck option or allow customizing the comparison behavior - it's entirely based on reference comparisons, since it uses WeakMap/Map internally. It also does not have a maxSize option, but does have resultEqualityCheck.

If you need to customize the overall equality comparison behavior, import and pass lruMemoize as the memoize and argsMemoize option!

Also, note that an "infinite cache size" from one point of view can be considered a "memory leak" for another point of view. The use of WeakMaps should mean that in most cases values do get garbage collected when the rest of the app no longer needs those, but there may be some scenarios with use of primitive keys that could lead to potential leaks. If this looks like it's happening for you, please compare behavior with lruMemoize instead, and file an issue report so we can investigate.

New / Improved createSelector Memoization Options

Originally, the only way to customize createSelector's behavior (such as using an alternate memoization function) was to first create a customized version via createSelectorCreator(memoizerFunction, memoizerOptions). This was typically used for creating use cases like deep equality comparisons with _.equal instead of shallow equality, as well as alternate memoizers that had a notion of cache size.

With Reselect 4.1.0, we added the ability to pass memoizer options directly to createSelector, and also updated defaultMemoize to accept several options such as a max cache size. This meant that you could call createSelector(...inputFns, outputFn, {memoizeOptions: {maxSize: 100}}), but you couldn't change the memoizer _function_ being used directly - that still required use of createSelectorCreator`.

Additionally, Reselect internally uses the provided memoizer function twice internally: once on the overall arguments passed to selectSomeValue(a, b, c), and a second time on the values extracted by the input functions such as state => state.a. There have been multiple issues over the years where users wanted to provide separate memoization functions for the arguments vs the extracted values, such as a reference equality check for the arguments and a shallow check for the extracted values.

With this release, you can now pass alternate memoizer functions directly to createSelector, and both createSelector and createSelectorCreator accept separate options for memoize and argsMemoize (along with any options for those):

const selectTodoIds = createSelector(
  (state: TodoState) => state.todos,
  todos => todos.map(({ id }) => id),
  {
    memoize: defaultMemoize,
    memoizeOptions: {
      resultEqualityCheck: (a, b) => a === b
    }
    argsMemoize: microMemoize,
    argsMemoizeOptions: { 
      isEqual: (a, b) => a === b 
    },
  }
)

This should mostly eliminate the need to use createSelectorCreator for customization. (You can still use it for encapsulation / reuse if you want to create many selectors with the same customization options.)

ESM/CJS Package Compatibility

The biggest theme of the Redux v5 and RTK 2.0 releases is trying to get "true" ESM package publishing compatibility in place, while still supporting CJS in the published package.

The primary build artifact is now an ESM file, dist/reselect.mjs. Most build tools should pick this up. There's also a CJS artifact, and a second copy of the ESM file named reselect.legacy-esm.js to support Webpack 4 (which does not recognize the exports field in package.json). Additionally, all of the build artifacts now live under ./dist/ in the published package.

Modernized Build Output

We now publish modern JS syntax targeting ES2020, including optional chaining, object spread, and other modern syntax. If you need to

Build Tooling

We're now building the package using https://github.com/egoist/tsup. We also now include sourcemaps for the ESM and CJS artifacts.

Dropping UMD Builds

Redux has always shipped with UMD build artifacts. These are primarily meant for direct import as script tags, such as in a CodePen or a no-bundler build environment.

We've dropped those build artifacts from the published package, on the grounds that the use cases seem pretty rare today.

There's now a reselect.browser.mjs file in the package that can be loaded from a CDN like Unpkg.

If you have strong use cases for us continuing to include UMD build artifacts, please let us know!

Dev Mode Checks

createSelector now does checks in development mode for common mistakes, like input selectors that always return new references, or result functions that immediately return their argument. These checks can be customized at selector creation or globally.

This is important, as an input selector returning a materially different result with the same parameters means that the output selector will never memoize correctly and be run unnecessarily, thus (potentially) creating a new result and causing rerenders.

const addNumbers = createSelector(
  // this input selector will always return a new reference when run
  // so cache will never be used
  (a, b) => ({ a, b }),
  ({ a, b }) => ({ total: a + b })
)
// instead, you should have an input selector for each stable piece of data
const addNumbersStable = createSelector(
  (a, b) => a,
  (a, b) => b,
  (a, b) => ({
    total: a + b,
  })
)

This is done the first time the selector is called, unless configured otherwise. See the Reselect docs on dev-mode checks for more details.

TypeScript Changes

We've dropped support for TS 4.6 and earlier, and our support matrix is now TS 4.7+.

The ParametricSelector and OutputParametricSelector types have been removed. Use Selector and OutputSelector instead.

The TS types have been updated to provide a better visual hover preview representation of a selector. It should now actually be previewed as "a function with attached fields", like:

const selectTodos: ((state: {
    todos: {
        id: number;
        title: string;
        description: string;
        completed: boolean;
    }[];
}) => number[]) & {
    clearCache: () => void;
    resultsCount: () => number;
    resetResultsCount: () => void;
} & {
    lastResult: () => number[];
    recomputations: () => number;
    resetRecomputations: () => void;
    dependencyRecomputations: () => number;
    resetDependencyRecomputations: () => void;
    // snip additional attached functions
} 

Other Changes

Selectors now have a dependencyRecomputions method that returns how many times the dependency memoizer recalculated, and a resetDependencyRecomputations method that resets that value.

Similarly, both weakMapMemoize and lruMemoize now have resultsCount and resetResultsCount methods that count how many times they actually calculated new values. This should be equal to the number of outer recomputations, unless you have passed in resultEqualityCheck as an option, in which case it only counts times a new actual reference was returned.

Huge thanks to @aryaemami59 for some incredibly comprehensive efforts reworking the internals of createSelector, our TS types, and the codebase structure in order to make all these changes possible!

What's Changed

Full Changelog: v4.1.8...v5.0.1

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.


Depfu Status

Depfu will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with @depfu rebase.

All Depfu comment commands
@​depfu rebase
Rebases against your default branch and redoes this update
@​depfu recreate
Recreates this PR, overwriting any edits that you've made to it
@​depfu merge
Merges this PR once your tests are passing and conflicts are resolved
@​depfu cancel merge
Cancels automatic merging of this PR
@​depfu close
Closes this PR and deletes the branch
@​depfu reopen
Restores the branch and reopens this PR (if it's closed)
@​depfu pause
Ignores all future updates for this dependency and closes this PR
@​depfu pause [minor|major]
Ignores all future minor/major updates for this dependency and closes this PR
@​depfu resume
Future versions of this dependency will create PRs again (leaves this PR as is)
julienw commented 2 weeks ago

https://github.com/reduxjs/reselect/issues/635 still doesn't seem fixed. It would be good to look at the impact for us, and if we need to implement https://github.com/reduxjs/reselect/issues/635#issuecomment-1850350607 for all or some of our selectors. Normally this should be handled in reselect's code in https://github.com/reduxjs/reselect/blob/2d17a06720c15aab52f682c42de909df5041d9a5/src/weakMapMemoize.ts#L205-L209 but it would be good to double check our memory usage with the new version.

Updating reselect is therefore a significant work for us without a clear improvement, so I'm not in a rush to update it...