diff --git a/.eslintrc b/.eslintrc index afacdf656..05bd4959c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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" + ] } } }, @@ -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 } -} +} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2aa758f78..03de6d6d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,17 @@ jobs: - name: Collect coverage run: yarn coverage + - name: Build + run: yarn build + + - name: Pack + run: yarn pack + + - uses: actions/upload-artifact@v2 + with: + name: package + path: ./package.tgz + test-types: name: Test Types with TypeScript ${{ matrix.ts }} @@ -61,3 +72,49 @@ jobs: run: | yarn tsc --version yarn type-tests + + test-published-artifact-local: + name: Test Published Artifact (Local) ${{ matrix.example }} + + needs: [build] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['16.x'] + example: ['rr-rsc-context'] + defaults: + run: + working-directory: ./examples/publish-ci/${{ matrix.example }} + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Use node ${{ matrix.node }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + + - name: Install deps + run: yarn install + + - name: Remove existing React-Redux + run: yarn remove react-redux + + - uses: actions/download-artifact@v2 + with: + name: package + path: ./examples/publish-ci/${{ matrix.example }} + + - name: Check folder contents + run: ls -l . + + - name: Install build artifact + run: yarn add ./package.tgz + + - name: Show installed React-Redux versions + run: yarn info react-redux && yarn why react-redux + + - name: Build example + run: yarn build diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 000000000..eb5c9f0c1 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,8 @@ +{ + "hooks": { + "after:bump": "yarn && git add -u" + }, + "git": { + "tagName": "v${version}" + } +} diff --git a/docs/api/Provider.md b/docs/api/Provider.md index 3c9ef6115..1ee84d727 100644 --- a/docs/api/Provider.md +++ b/docs/api/Provider.md @@ -43,6 +43,9 @@ interface ProviderProps { */ context?: Context> + /** Global configuration for the `useSelector` stability check */ + stabilityCheck?: StabilityCheck + /** The top-level React elements in your component tree, such as `` **/ children: ReactNode } diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 337354ac2..90ba18fdd 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -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. @@ -44,27 +44,43 @@ From there, you may import any of the listed React Redux hooks APIs and use them ## `useSelector()` -```js -const result: any = useSelector(selector: Function, equalityFn?: Function) +```ts +type RootState = ReturnType +type SelectorFn = (state: RootState) => Selected +type EqualityFn = (a: any, b: any) => boolean +export type CheckFrequency = 'never' | 'once' | 'always' + +interface UseSelectorOptions { + equalityFn?: EqualityFn + stabilityCheck?: CheckFrequency + noopCheck?: CheckFrequency +} + +const result: Selected = useSelector( + selector: SelectorFunction, + options?: EqualityFn | UseSelectorOptions +) ``` -Allows you to extract data from the Redux store state, using a selector function. +Allows you to extract data from the Redux store state for use in this component, using a selector function. :::info The selector function should be [pure](https://en.wikipedia.org/wiki/Pure_function) since it is potentially executed multiple times and at arbitrary points in time. +See [Using Redux: Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors) in the Redux docs for more details on writing and using selector functions. + ::: -The selector is approximately equivalent to the [`mapStateToProps` argument to `connect`](../using-react-redux/connect-extracting-data-with-mapStateToProps.md) conceptually. The selector will be called with the entire Redux store state as its only argument. The selector will be run whenever the function component renders (unless its reference hasn't changed since a previous render of the component so that a cached result can be returned by the hook without re-running the selector). `useSelector()` will also subscribe to the Redux store, and run your selector whenever an action is dispatched. +The selector will be called with the entire Redux store state as its only argument. The selector may return any value as a result, including directly returning a value that was nested inside `state`, or deriving new values. The return value of the selector will be used as the return value of the `useSelector()` hook. + +The selector will be run whenever the function component renders (unless its reference hasn't changed since a previous render of the component so that a cached result can be returned by the hook without re-running the selector). `useSelector()` will also subscribe to the Redux store, and run your selector whenever an action is dispatched. -However, there are some differences between the selectors passed to `useSelector()` and a `mapState` function: +When an action is dispatched, `useSelector()` will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. `useSelector()` uses strict `===` reference equality checks by default, not shallow equality (see the following section for more details). -- The selector may return any value as a result, not just an object. The return value of the selector will be used as the return value of the `useSelector()` hook. -- When an action is dispatched, `useSelector()` will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render. -- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples below) or by using a curried selector. -- Extra care must be taken when using memoizing selectors (see examples below for more details). -- `useSelector()` uses strict `===` reference equality checks by default, not shallow equality (see the following section for more details). +The selector is approximately equivalent to the [`mapStateToProps` argument to `connect`](../using-react-redux/connect-extracting-data-with-mapStateToProps.md) conceptually. + +You may call `useSelector()` multiple times within a single function component. Each call to `useSelector()` creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple `useSelector()`s in the same component to return new values _should_ only result in a single re-render. :::info @@ -72,8 +88,6 @@ There are potential edge cases with using props in selectors that may cause issu ::: -You may call `useSelector()` multiple times within a single function component. Each call to `useSelector()` creates an individual subscription to the Redux store. Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple `useSelector()`s in the same component to return new values _should_ only result in a single re-render. - ### Equality Comparisons and Updates When the function component renders, the provided selector function will be called and its result will be returned @@ -96,8 +110,13 @@ every time will _always_ force a re-render by default. If you want to retrieve m ```js import { shallowEqual, useSelector } from 'react-redux' -// later +// Pass it as the second argument directly const selectedData = useSelector(selectorReturningObject, shallowEqual) + +// or pass it as the `equalityFn` field in the options argument +const selectedData = useSelector(selectorReturningObject, { + equalityFn: shallowEqual, +}) ``` - Use a custom equality function as the `equalityFn` argument to `useSelector()`, like: @@ -240,10 +259,96 @@ export const App = () => { } ``` +### Development mode checks + +`useSelector` runs some extra checks in development mode to watch for unexpected behavior. These checks do not run in production builds. + +:::info + +These checks were first added in v8.1.0 + +::: + +#### Selector result stability + +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 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, +// which causes the component to rerender after *every* action is dispatched +const { count, user } = useSelector((state) => ({ + count: state.count, + user: state.user, +})) +``` + +If a selector result is suitably stable (or the selector is memoized), it will not return a different result and no warning will be logged. + +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" + + {children} + +``` + +```tsx title="Individual hook setting" +function Component() { + const count = useSelector(selectCount, { stabilityCheck: 'never' }) + // run once (default) + const user = useSelector(selectUser, { stabilityCheck: 'once' }) + // ... +} +``` + +#### 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" + + {children} + +``` + +```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: + +- The selector may return any value as a result, not just an object. +- The selector normally _should_ return just a single value, and not an object. If you do return an object or an array, be sure to use a memoized selector to avoid unnecessary re-renders. +- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples above) or by using a curried selector. +- You can use the `equalityFn` option to customize the comparison behavior + ## `useDispatch()` -```js -const dispatch = useDispatch() +```ts +import type { Dispatch } from 'redux' +const dispatch: Dispatch = useDispatch() ``` This hook returns a reference to the `dispatch` function from the Redux store. You may use it to dispatch actions as needed. @@ -268,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' @@ -319,8 +424,9 @@ export const Todos = () => { ## `useStore()` -```js -const store = useStore() +```ts +import type { Store } from 'redux' +const store: Store = useStore() ``` This hook returns a reference to the same Redux store that was passed in to the `` component. @@ -333,12 +439,19 @@ This hook should probably not be used frequently. Prefer `useSelector()` as your import React from 'react' import { useStore } from 'react-redux' -export const CounterComponent = ({ value }) => { +export const ExampleComponent = ({ value }) => { const store = useStore() + const onClick = () => { + // Not _recommended_, but safe + // This avoids subscribing to the state via `useSelector` + // Prefer moving this logic into a thunk instead + const numTodos = store.getState().todos.length + } + // EXAMPLE ONLY! Do not do this in a real app. // The component will not automatically update if the store state changes - return
{store.getState()}
+ return
{store.getState().todos.length}
} ``` diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md index 83b420337..11da5346a 100644 --- a/docs/introduction/getting-started.md +++ b/docs/introduction/getting-started.md @@ -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 diff --git a/docs/tutorials/quick-start.md b/docs/tutorials/quick-start.md index fc22ef1cd..e0802e135 100644 --- a/docs/tutorials/quick-start.md +++ b/docs/tutorials/quick-start.md @@ -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) ::: @@ -105,7 +105,8 @@ export const counterSlice = createSlice({ // Redux Toolkit allows us to write "mutating" logic in reducers. It // doesn't actually mutate the state because it uses the Immer library, // which detects changes to a "draft state" and produces a brand new - // immutable state based off those changes + // immutable state based off those changes. + // Also, no return statement is required from these functions. state.value += 1 }, decrement: (state) => { @@ -125,7 +126,7 @@ export default counterSlice.reducer ### Add Slice Reducers to the Store -Next, we need to import the reducer function from the counter slice and add it to our store. By defining a field inside the `reducers` parameter, we tell the store to use this slice reducer function to handle all updates to that state. +Next, we need to import the reducer function from the counter slice and add it to our store. By defining a field inside the `reducer` parameter, we tell the store to use this slice reducer function to handle all updates to that state. ```js title="app/store.js" import { configureStore } from '@reduxjs/toolkit' @@ -206,9 +207,7 @@ That was a brief overview of how to set up and use Redux Toolkit with React. Rec ### Full Counter App Example -The counter example app shown here is also the - -Here's the complete counter application as a running CodeSandbox: +Here's the complete Counter application as a running CodeSandbox: