• React Redux: Hooks
  • Translator: Tashi
  • Protocol: CC BY-NC-SA 4.0

Hooks

React’s new “hooks” APIs give function components the ability to use local component state, perform side effects, and more.

React Redux now provides a set of hook APIs as an alternative to the current connect() higher-level component. These APIs allow you to subscribe to Redux’s Store and dispatch actions without using connect() to wrap the component.

These hooks were first added in version V7.1.0.

Use hooks in a React Redux application

As with connect(), you should first wrap the entire application in , exposing the Store to the entire component tree.

const store = createStore(rootReducer)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root'))Copy the code

You can then import the React Redux hooks APIs listed below and use them in function components.

useSelector()

const result : any = useSelector(selector : Function, equalityFn? : Function)
Copy the code

By passing in the selector, you get the state data from Redux’s store.

Warning: The selector function should be pure, because, at any point in time, it can be executed many times.

Conceptually, the selector function takes roughly the same parameters as connect’s mapStateToProps. When the selector function is called, it will be passed the entire state of the Redux store as the only argument. Every time a function component is rendered, the selector function is called. UseSelector () will also subscribe to Redux’s sotre and will be executed every time you dispatch an action.

However, the various selector functions passed to useSelector() are a little different from the mapState function:

  • The selector function can return any type of value, and it doesn’t have to be an object. The return value of the selector function is used as the calluseSelector()The value returned by hook.
  • When an action is dispatched,useSelector()The result of the last call to the selector is compared by reference (===) to the result of the current call, and if it is different, the component is forced to rerender. If it is, it will not be re-rendered.
  • The selector doesn’t receive itownPropsParameters. But props can be obtained using closures (here’s an example) or by using a Curricized selector function.
  • Some additional care is required when using selectors with Memoizing (here’s an example to help you understand).
  • useSelector()Strict comparison is used by default= = =To compare references, not shallow comparisons. (See section below for details.)

A shallow comparison does not mean that == Strict comparison === corresponds to loose comparison ==, and shallow comparison corresponds to deep comparison.

Warning: There are some boundary cases that can cause errors when using props in selectors. See the usage warning section of this page.

You can call useSelector() multiple times within a function component. Each call to useSelector() creates a separate subscription to Redux’s store. Because of the Update batching behavior of Redux V7, if a dispatched action causes multiple useselectors () in a component to produce new values, Only one re-render will be triggered.

Equality Literacy and Renewal

When a function component is rendered, the selector function passed in is called, and the result is returned as the return value of useSelector(). (If the selector has already been executed and hasn’t changed, it might return the cached result.)

However, when an action is dispatched to the Redux store, useSelector() will only trigger a rerender if the result executed by the selector is different from the last one. In version V7.1.0-alpha.5, the default comparison mode is strict reference comparison ===. This is different from connect(), which uses shallow comparisons to compare the results of mapState execution to determine whether rerendering is triggered. Here are some suggestions on how to use useSelector().

For mapState, all independent state fields are returned by binding to an object. It doesn’t matter if the reference to the returned object is new — connect() compares each field individually. For useSelector(), returning a new object reference always triggers rerendering as the default behavior of useSelector(). If you want to get multiple values from the store, you can:

  • UseSelector () is called multiple times, each time returning the value of a single field

  • Use Reselect or a similar library to create a memorized selector function that returns multiple values in an object, but returns a new object only when one of the values changes.

  • Use the react-redux shallowEqual function as the equalityFn argument to useSelector(), as in:

import { shallowEqual, useSelector } from 'react-redux'

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Copy the code

This optional comparison argument allows us to use Lodash’s _.isequal () or immutable.js comparison capabilities.

UseSelector example

Basic usage:

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = (a)= > {
  const counter = useSelector(state= > state.counter)
  return <div>{counter}</div>
}
Copy the code

Using the closure to select what state to retrieve using props:

import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props= > {
  const todo = useSelector(state= > state.todos[props.id])
  return <div>{todo.text}</div>
}
Copy the code

Use memorized selectors

Using the single-line arrow function when using useSelector, as shown above, causes a new selector function to be created during each rendering. As you can see, this selector doesn’t maintain any internal state. However, memorized selectors (created through createSelector in the ResELECT library) contain internal states, so you must be careful when using them.

When a selector depends on a state, make sure that the function declaration is outside the component, so that the same selector is not created repeatedly every time it is rendered:

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumOfDoneTodos = createSelector(
  state= > state.todos,
  todos => todos.filter(todo= > todo.isDone).length
)

export const DoneTodosCounter = (a)= > {
  const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
  return <div>{NumOfDoneTodos}</div>
}

export const App = (a)= > {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>)}Copy the code

This is also true for dependencies on component props, but only in the form of a singleton component

import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const selectNumOfTodosWithIsDoneValue = createSelector(
  state= > state.todos,
  (_, isDone) => isDone,
  (todos, isDone) => todos.filter(todo= > todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({ isDone }) = > {
  const NumOfTodosWithIsDoneValue = useSelector(state= >
    selectNumOfTodosWithIsDoneValue(state, isDone)
  )

  return <div>{NumOfTodosWithIsDoneValue}</div>
}

export const App = (a)= > {
  return (
    <>
      <span>Number of done todos:</span>
      <TodoCounterForIsDoneValue isDone={true} />
    </>
  )
}
Copy the code

If you want to use the same selector function for multiple component instances that depends on the component props, you must make sure that each component instance creates its own selector function (here’s why this is necessary).

import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'

const makeNumOfTodosWithIsDoneSelector = (a)= >
  createSelector(
    state= > state.todos,
    (_, isDone) => isDone,
    (todos, isDone) => todos.filter(todo= > todo.isDone === isDone).length
  )

export const TodoCounterForIsDoneValue = ({ isDone }) = > {
  const selectNumOfTodosWithIsDone = useMemo(
    makeNumOfTodosWithIsDoneSelector,
    []
  )

  const numOfTodosWithIsDoneValue = useSelector(state= >
    selectNumOfTodosWithIsDone(state, isDone)
  )

  return <div>{numOfTodosWithIsDoneValue}</div>
}

export const App = (a)= > {
  return (
    <>
      <span>Number of done todos:</span>
      <TodoCounterForIsDoneValue isDone={true} />
      <span>Number of unfinished todos:</span>
      <TodoCounterForIsDoneValue isDone={false} />
    </>
  )
}
Copy the code

Removed:useActions()

UseActions () has been removed

useDispatch()

const dispatch = useDispatch()
Copy the code

This hook returns a reference to the Redux Store’s dispatch function. You might use it to dispatch some needed actions.

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) = > {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={()= > dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>)}Copy the code

When passing a callback that uses the Dispatch function to a child component, it is recommended to use the useCallback function to memorise the callback function to prevent unnecessary rendering due to changes in the callback reference.

Translator’s note: This advice has nothing to do with Dispatch. Whether you use Dispatch or not, you should make sure that the callback doesn’t change for no reason and cause unnecessary rerendering. It doesn’t matter because useCallback recreates the callback function whenever the Dispatch changes, and the reference to the callback function must have changed, causing unnecessary rerendering.

import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) = > {
  const dispatch = useDispatch()
  const incrementCounter = useCallback(
    (a)= > dispatch({ type: 'increment-counter' }),
    [dispatch]
  )

  return (
    <div>
      <span>{value}</span>
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}

export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))
Copy the code

useStore()

const store = useStore()
Copy the code

This hook returns a reference to Redux Sotore passed to the component.

This hook should probably not be used too often. You should use useSelector() as your first choice. However, this can be useful in some unusual scenarios where you need to access the Store, such as replacing the Store reducers.

example

import React from 'react'
import { useStore } from 'react-redux'

export const CounterComponent = ({ value }) = > {
  const store = useStore()

  // EXAMPLE ONLY! Do not do this in a real app.
  // The component will not automatically update if the store state changes
  return <div>{store.getState()}</div>
}
Copy the code

Custom context

The component allows you to specify an optional context with the context parameter. This feature is useful when you’re building complex reusable components that you don’t want to have your own private store collide with the Redux store of the users that use the component,

Create custom hooks by using the Hook Creator function to access the optional context.

import React from 'react'
import {
  Provider,
  createStoreHook,
  createDispatchHook,
  createSelectorHook
} from 'react-redux'

const MyContext = React.createContext(null)

// Export your custom hooks if you wish to use them in other files.
export const useStore = createStoreHook(MyContext)
export const useDispatch = createDispatchHook(MyContext)
export const useSelector = createSelectorHook(MyContext)

const myStore = createStore(rootReducer)

export function MyProvider({ children }) {
  return (
    <Provider context={MyContext} store={myStore}>
      {children}
    </Provider>
  )
}
Copy the code

Use of warning

Expired Props and “Zombie sub-components”

One difficulty with the React Redux implementation is that when you define the mapStateToProps function as (state, ownProps), how do you ensure that the mapStateToProps is called each time with the latest props? In Version 4, bugs often occur in edge cases, such as an error thrown inside the mapState function when an item in a list is deleted.

Starting with Version 5, React Redux attempted to ensure that the ownProps parameters were consistent. In Version 7, this guarantee is achieved by using a custom Subscription class inside Connect (), which also results in layers of components being nested. This ensures that components deep in the component tree after CONNECT () will only be notified that the Store has updated when their nearest ancestor after connect() has updated. However, this depends on the context inside the React part of each connect() instance, and connect() then provides its own unique Subscription instance to nest components in, Provide a new conext values for < ReactReduxContext. The Provider >, then apply colours to a drawing.

The use of hooks, means that cannot render the < ReactReduxContext. The Provider >, also means that there is no nested subscription level. Therefore, the issue of “expired Props” and “zombie sub-components” can happen again if you use hooks instead of connect() applications.

In detail, what can happen to “expired Props” is:

  • A selector function relies on the component’s props to get the data back.
  • After an action is distributed, the parent component will re-render and pass the new props to the child component
  • But the selector function of the child component is executed before the child component is rendered with the new props.

Depending on what the current state of the props and stroe being used is, this can cause incorrect data to be returned or even an error to be thrown.

The term “zombie child component” refers specifically to the following situations:

  • Initially, multiple components nested after connect() are mounted together, causing the child component to subscribe before its parent.

  • An action is dispatched to remove some data from the store, such as a to-do.

  • The parent component stops rendering the corresponding child component

  • However, because the child subscribes before the parent, its subscribing callback runs before the parent stops rendering the child. When the child component retrieves the data corresponding to the props, the data is no longer there, and if the logic of the fetch code is not careful, an error may be thrown.

UseSelector () addresses the “zombie child component” problem by catching all internal selector errors thrown by store updates (but not errors caused by render updates). When an error occurs, the component is forced to rerender, at which point the selector function is executed again. Note that this strategy only works if your selector function is pure and your code doesn’t depend on some custom error thrown by a selector.

If you’d rather deal with these issues yourself, here are some tips that might help you avoid them when using useSelector().

  • Don’t rely on props to get data back in the selector function.

  • Try to have defensive selectors in cases where you have to rely on props, and the props change a lot, and where the data you get back might get deleted. Do not fetch data directly, for example: state.todos[props. Id].name – get state.todos[props. Id], check if the value exists, and then try to get todo.name

  • Because connect adds the necessary Subscription component to the Context provider, and delays the execution of the subcomponent Subscription until the connect() component is rerendered, in the component tree, Placing a connect() component on top of a component that uses useSelector will avoid this problem, as long as the connect() component and the child that uses hooks to trigger rerender are caused by the same store update.

Note: If you want a more detailed description of this issue, this chat details the issue, as well as issue #1179.

performance

As mentioned above, after an action is dispatched (dispatch), useSelector() defaults to reference the return value of the select function ===, and triggers rerendering only if the return value changes. However, unlike connect(), useSelector() does not prevent parent component rerendering from causing child component rerendering, even if component props have not changed.

If you want further optimizations like this, you might want to consider wrapping your function components in react.Memo () :

const CounterComponent = ({ name }) = > {
  const counter = useSelector(state= > state.counter)
  return (
    <div>
      {name}: {counter}
    </div>)}export const MemoizedCounterComponent = React.memo(CounterComponent)
Copy the code

Hooks formula

We streamlined the hooks API from the original alpha release to focus on smaller, more basic apis. However, in your application, you may still want to use some of the methods we implemented earlier. The code in the following example is ready to be copied and used in your code base.

Formula:useActions()

This hook existed in the original alpha release, but was removed on Dan Abramov’s recommendation in v7.1.0-alpha.4. The recommendation indicates that in the Hook scenario, “binding action Creators” is not as useful as it used to be, and may result in more conceptual understanding and syntactic complexity.

The translator’s note:action creatorsThe function used to generate the Action object.

In components, you would prefer to use the useDispatch hook to get a reference to the Dispatch function, and then call Dispatch (someActionCreator()) or some desired side effect manually in the callback function. In your code, you can still use the bindActionCreators function to bindActionCreators, or you can also bind them manually. For example, const boundAddTodo = (text) => Dispatch (addTodo(text)).

However, if you want to use the hook yourself, there is a copy-ready version that supports the Action Creators, passed in as a standalone function, array, or object.

import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'

export function useActions(actions, deps) {
  const dispatch = useDispatch()
  return useMemo((a)= > {
    if (Array.isArray(actions)) {
      return actions.map(a= > bindActionCreators(a, dispatch))
    }
    return bindActionCreators(actions, dispatch)
  }, deps ? [dispatch, ...deps] : [dispatch])
}
Copy the code

Formula:useShallowEqualSelector()

import { useSelector, shallowEqual } from 'react-redux'

export function useShallowEqualSelector(selector) {
  return useSelector(selector, shallowEqual)
}
Copy the code