Skip to content

Commit

Permalink
Merge pull request #2022 from reduxjs/no-op-check
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Jun 13, 2023
2 parents 1812a78 + a18e8a9 commit bf08ea6
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 122 deletions.
32 changes: 26 additions & 6 deletions .eslintrc
Expand Up @@ -11,10 +11,16 @@
"react": {
"version": "detect"
},
"import/ignore": ["react-native"],
"import/ignore": [
"react-native"
],
"import/resolver": {
"node": {
"extensions": [".js", ".ts", ".tsx"]
"extensions": [
".js",
".ts",
".tsx"
]
}
}
},
Expand All @@ -38,12 +44,26 @@
"react/jsx-wrap-multilines": 2,
"react/no-string-refs": 0,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/no-unused-vars": [
"error"
],
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"]
"@typescript-eslint/no-redeclare": [
"error"
],
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports"
}
]
},
"plugins": ["@typescript-eslint", "import", "react"],
"plugins": [
"@typescript-eslint",
"import",
"react"
],
"globals": {
"JSX": true
}
}
}
43 changes: 38 additions & 5 deletions docs/api/hooks.md
Expand Up @@ -10,7 +10,7 @@ description: 'API > Hooks: the `useSelector` and `useDispatch` hooks`'

# Hooks

React's new ["hooks" APIs](https://reactjs.org/docs/hooks-intro.html) give function components the ability to use local component state, execute side effects, and more. React also lets us write [custom hooks](https://reactjs.org/docs/hooks-custom.html), which let us extract reusable hooks to add our own behavior on top of React's built-in hooks.
React's ["hooks" APIs](https://react.dev/reference/react#) give function components the ability to use local component state, execute side effects, and more. React also lets us write [custom hooks](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component), which let us extract reusable hooks to add our own behavior on top of React's built-in hooks.

React Redux includes its own custom hook APIs, which allow your React components to subscribe to the Redux store and dispatch actions.

Expand Down Expand Up @@ -48,11 +48,12 @@ From there, you may import any of the listed React Redux hooks APIs and use them
type RootState = ReturnType<typeof store.getState>
type SelectorFn = <Selected>(state: RootState) => Selected
type EqualityFn = (a: any, b: any) => boolean
export type StabilityCheck = 'never' | 'once' | 'always'
export type CheckFrequency = 'never' | 'once' | 'always'

interface UseSelectorOptions {
equalityFn?: EqualityFn
stabilityCheck?: StabilityCheck
stabilityCheck?: CheckFrequency
noopCheck?: CheckFrequency
}

const result: Selected = useSelector(
Expand Down Expand Up @@ -272,7 +273,7 @@ These checks were first added in v8.1.0

In development, the provided selector function is run an extra time with the same parameter during the first call to `useSelector`, and warns in the console if the selector returns a different result (based on the `equalityFn` provided).

This is important, as a selector returning that returns a different result reference with the same parameter will cause unnecessary rerenders.
This is important, as **a selector that returns a different result reference when called again with the same inputs will cause unnecessary rerenders**.

```ts
// this selector will return a new object reference whenever called,
Expand Down Expand Up @@ -302,6 +303,38 @@ function Component() {
}
```

#### No-op selector check

In development, a check is conducted on the result returned by the selector. It warns in the console if the result is the same as the parameter passed in, i.e. the root state.

**A `useSelector` call returning the entire root state is almost always a mistake**, as it means the component will rerender whenever _anything_ in state changes. Selectors should be as granular as possible, like `state => state.some.nested.field`.

```ts no-transpile
// BAD: this selector returns the entire state, meaning that the component will rerender unnecessarily
const { count, user } = useSelector((state) => state)

// GOOD: instead, select only the state you need, calling useSelector as many times as needed
const count = useSelector((state) => state.count.value)
const user = useSelector((state) => state.auth.currentUser)
```

By default, this will only happen when the selector is first called. You can configure the check in the Provider or at each `useSelector` call.

```tsx title="Global setting via context"
<Provider store={store} noopCheck="always">
{children}
</Provider>
```

```tsx title="Individual hook setting"
function Component() {
const count = useSelector(selectCount, { noopCheck: 'never' })
// run once (default)
const user = useSelector(selectUser, { noopCheck: 'once' })
// ...
}
```

### Comparisons with `connect`

There are some differences between the selectors passed to `useSelector()` and a `mapState` function:
Expand Down Expand Up @@ -340,7 +373,7 @@ export const CounterComponent = ({ value }) => {
}
```

When passing a callback using `dispatch` to a child component, you may sometimes want to memoize it with [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback). _If_ the child component is trying to optimize render behavior using `React.memo()` or similar, this avoids unnecessary rendering of child components due to the changed callback reference.
When passing a callback using `dispatch` to a child component, you may sometimes want to memoize it with [`useCallback`](https://react.dev/reference/react/useCallback). _If_ the child component is trying to optimize render behavior using `React.memo()` or similar, this avoids unnecessary rendering of child components due to the changed callback reference.

```jsx
import React, { useCallback } from 'react'
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/getting-started.md
Expand Up @@ -13,7 +13,7 @@ import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'

# Getting Started with React Redux

[React Redux](https://github.com/reduxjs/react-redux) is the official [React](https://reactjs.org/) UI bindings layer for [Redux](https://redux.js.org/). It lets your React components read data from a Redux store, and dispatch actions to the store to update state.
[React Redux](https://github.com/reduxjs/react-redux) is the official [React](https://react.dev/) UI bindings layer for [Redux](https://redux.js.org/). It lets your React components read data from a Redux store, and dispatch actions to the store to update state.

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/quick-start.md
Expand Up @@ -18,7 +18,7 @@ hide_title: true
:::info Prerequisites

- Familiarity with [ES6 syntax and features](https://www.taniarascia.com/es6-syntax-and-feature-overview/)
- Knowledge of React terminology: [JSX](https://reactjs.org/docs/introducing-jsx.html), [State](https://reactjs.org/docs/state-and-lifecycle.html), [Function Components, Props](https://reactjs.org/docs/components-and-props.html), and [Hooks](https://reactjs.org/docs/hooks-intro.html)
- Knowledge of React terminology: [JSX](https://react.dev/learn/writing-markup-with-jsx), [State](https://react.dev/learn/state-a-components-memory), [Function Components, Props](https://react.dev/learn/passing-props-to-a-component), and [Hooks](https://react.dev/reference/react#)
- Understanding of [Redux terms and concepts](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow)

:::
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/typescript.md
Expand Up @@ -17,7 +17,7 @@ hide_title: true

:::info Prerequisites

- Knowledge of React [Hooks](https://reactjs.org/docs/hooks-intro.html)
- Knowledge of React [Hooks](https://react.dev/reference/react#)
- Understanding of [Redux terms and concepts](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow)
- Understanding of TypeScript syntax and concepts

Expand Down
2 changes: 1 addition & 1 deletion docs/using-react-redux/accessing-store.md
Expand Up @@ -22,7 +22,7 @@ connected components, or access the store directly. Here are some examples of ho

## Understanding Context Usage

Internally, React Redux uses [React's "context" feature](https://reactjs.org/docs/context.html) to make the
Internally, React Redux uses [React's "context" feature](https://react.dev/learn/passing-data-deeply-with-context) to make the
Redux store accessible to deeply nested connected components. As of React Redux version 6, this is normally handled
by a single default context object instance generated by `React.createContext()`, called `ReactReduxContext`.

Expand Down
5 changes: 3 additions & 2 deletions src/components/Context.ts
Expand Up @@ -2,7 +2,7 @@ import { createContext } from 'react'
import type { Context } from 'react'
import type { Action, AnyAction, Store } from 'redux'
import type { Subscription } from '../utils/Subscription'
import { StabilityCheck } from '../hooks/useSelector'
import type { CheckFrequency } from '../hooks/useSelector'

export interface ReactReduxContextValue<
SS = any,
Expand All @@ -11,7 +11,8 @@ export interface ReactReduxContextValue<
store: Store<SS, A>
subscription: Subscription
getServerState?: () => SS
stabilityCheck: StabilityCheck
stabilityCheck: CheckFrequency
noopCheck: CheckFrequency
}

let realContext: Context<ReactReduxContextValue> | null = null
Expand Down
19 changes: 13 additions & 6 deletions src/components/Provider.tsx
@@ -1,9 +1,11 @@
import React, { Context, ReactNode, useMemo } from 'react'
import { ReactReduxContext, ReactReduxContextValue } from './Context'
import type { Context, ReactNode } from 'react'
import React, { useMemo } from 'react'
import type { ReactReduxContextValue } from './Context'
import { ReactReduxContext } from './Context'
import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { Action, AnyAction, Store } from 'redux'
import { StabilityCheck } from '../hooks/useSelector'
import type { Action, AnyAction, Store } from 'redux'
import type { CheckFrequency } from '../hooks/useSelector'

export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
/**
Expand All @@ -24,7 +26,10 @@ export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
context?: Context<ReactReduxContextValue<S, A>>

/** Global configuration for the `useSelector` stability check */
stabilityCheck?: StabilityCheck
stabilityCheck?: CheckFrequency

/** Global configuration for the `useSelector` no-op check */
noopCheck?: CheckFrequency

children: ReactNode
}
Expand All @@ -35,6 +40,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
children,
serverState,
stabilityCheck = 'once',
noopCheck = 'once',
}: ProviderProps<A, S>) {
const contextValue = useMemo(() => {
const subscription = createSubscription(store)
Expand All @@ -43,8 +49,9 @@ function Provider<A extends Action = AnyAction, S = unknown>({
subscription,
getServerState: serverState ? () => serverState : undefined,
stabilityCheck,
noopCheck,
}
}, [store, serverState, stabilityCheck])
}, [store, serverState, stabilityCheck, noopCheck])

const previousState = useMemo(() => store.getState(), [store])

Expand Down
13 changes: 8 additions & 5 deletions src/components/connect.tsx
@@ -1,6 +1,7 @@
/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
import hoistStatics from 'hoist-non-react-statics'
import React, { ComponentType, useContext, useMemo, useRef } from 'react'
import type { ComponentType } from 'react'
import React, { useContext, useMemo, useRef } from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'

import type { Store } from 'redux'
Expand All @@ -14,27 +15,29 @@ import type {
ConnectPropsMaybeWithoutContext,
} from '../types'

import defaultSelectorFactory, {
import type {
MapStateToPropsParam,
MapDispatchToPropsParam,
MergeProps,
MapDispatchToPropsNonObject,
SelectorFactoryOptions,
} from '../connect/selectorFactory'
import defaultSelectorFactory from '../connect/selectorFactory'
import { mapDispatchToPropsFactory } from '../connect/mapDispatchToProps'
import { mapStateToPropsFactory } from '../connect/mapStateToProps'
import { mergePropsFactory } from '../connect/mergeProps'

import { createSubscription, Subscription } from '../utils/Subscription'
import type { Subscription } from '../utils/Subscription'
import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import shallowEqual from '../utils/shallowEqual'
import warning from '../utils/warning'

import {
ReactReduxContext,
import type {
ReactReduxContextValue,
ReactReduxContextInstance,
} from './Context'
import { ReactReduxContext } from './Context'

import type { uSES } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
Expand Down
4 changes: 2 additions & 2 deletions src/connect/wrapMapToProps.ts
@@ -1,6 +1,6 @@
import { ActionCreatorsMapObject, Dispatch, ActionCreator } from 'redux'
import type { ActionCreatorsMapObject, Dispatch, ActionCreator } from 'redux'

import { FixTypeLater } from '../types'
import type { FixTypeLater } from '../types'
import verifyPlainObject from '../utils/verifyPlainObject'

type AnyState = { [key: string]: any }
Expand Down
10 changes: 4 additions & 6 deletions src/hooks/useDispatch.ts
@@ -1,10 +1,8 @@
import { Action, AnyAction, Dispatch } from 'redux'
import { Context } from 'react'
import type { Action, AnyAction, Dispatch } from 'redux'
import type { Context } from 'react'

import {
ReactReduxContext,
ReactReduxContextValue,
} from '../components/Context'
import type { ReactReduxContextValue } from '../components/Context'
import { ReactReduxContext } from '../components/Context'
import { useStore as useDefaultStore, createStoreHook } from './useStore'

/**
Expand Down

0 comments on commit bf08ea6

Please sign in to comment.