简体   繁体   中英

Use Parameterized Selector (Selector Factory) in React Functional Components with useSelector from React-Redux

Parameterized selector / selector factory

It's a function accepts parameters to generate selector, as we can imagine, each time the function is called, a new selector is generated, see below example:

const makeGetSomeSelector = ({ param1, param2, ...}) =>
    createSelector(
       parentSelector,        // suppose it's declared before
       result =>  result.getIn([param1, param2, ...], defaultValue)
    )

General case

Now, we want to use it in React functional component, and since react-redux introduced useSelector , and based on its provided example , in component it should be look like this:

import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { makeGetSomeSelector } from 'XXX/selectors.js';

const MyComponent = ({ param1, param2, ... }) => {
    const getSomeSelector = useMemo(
       () => makeGetSomeSelector({param1, param2, ...}),
       [param1, param2, ...]
    );
    const selectorResult = useSelector(getSomeSelector);
    ...
}      

Case when it comes to constants

So far, everything make sense. However, there is a case that the parameters are not dynamic (eg from props, states, etc.), instead, they are constants! Then it becomes:

import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { makeGetSomeSelector } from 'XXX/selectors.js';
import { const1, const2, ... } from 'XXX/constants.js';

const MyComponent = () => {
    const getSomeSelector = useMemo(
       () => makeGetSomeSelector({const1, const2, ...}),
       []          // note that here we don't have any dependencies.
    );
    const selectorResult = useSelector(getSomeSelector);
    ...
}      

However someone asked me why not use the following way:

import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { makeGetSomeSelector } from 'XXX/selectors.js';
import { const1, const2, ... } from 'XXX/constants.js';

const getSomeSelector = makeGetSomeSelector({const1, const2, ...});
// declare outside of component, since all params are just constants.

const MyComponent = () => {
    const selectorResult = useSelector(getSomeSelector);
    ...
}

Question

Which one is correct one or both are fine?

The correctness means the result (and potentially, all intermediate results) of selector is indeed memorized/cached and won't lead to some problem like new selector is created and lost memoize feature ?

If anyone could elaborate how the selector factory works with useSelector (beside react-redux's example) will be more helpful!

The static selector can be used when it is called only once. For example: you have a list of items that can be filtered. The items are in state.data and the filters come from props then you can do something like this:

import { createSelector, defaultMemoize } from 'reselect';
const makeMemArray = () => {
  const mem = defaultMemoize((...array) => array);
  return (...array) => mem.apply(undefined, array);
};
const createSelectFilteredData = createSelector(
  (filter) => filter,
  (filter) => {
    const memArray = makeMemArray();
    //if data is [1,2,3] and after filter you get [1,2]
    //  and the next render data is [1,2] and the filter
    //  still returns [1,2] it will not be memoized because
    //  the array returned by array.filter is a new array
    //  you can get arround this by using makeMemArray
    return createSelector(
      (state) => state.data,
      (data) => memArray(filterData(data, filter))
    );
  }
);
const Component = ({filter})=>{
  //the filter parameter is curried, createSelectFilteredData(filter)
  //  returns a memoized function that will be re created when
  //  filter changes, that function takes state and will filter
  //  state.data using filter value(s)
  const items = useSelector(createSelectFilteredData(filter))
}

But if the situation is slightly different and you have a list of items where you want to get one item from then there will be multiple Item components using the selector with different item.id and nothing will be memoized. In that case you can use React.useMemo as you did in your example:

const createSelectItem = (itemId) =>
  createSelector(
    (state) => state.items,
    (items) => items.find(({ id }) => id === itemId)
  );
const Item = ({ id }) => {
  //Each Item has it's own selectItem selector that is
  //  (re) created only when id changes
  const selectItem = React.useMemo(
    () => createSelectItem(id),
    [id]
  );
  const item = useSelector(selectItem);
  //if you were to do const item = useSelector(createSelectItem(id))
  //  then nothing will be memoized because selector is re created
  //  on every render.
  //Even if you would define createSelectItem as previous example
  //  of createSelectFilteredData and List would have ids [1,2,3]
  //  then the first time you crate the selector it will get 1 passed
  //  the second time it is called it gets 2 passed the 3rd time 3
  //  next render the first time it's called with 1 so noting memoized
  //  because the last time it was called with 3, then it's called with 2
  //  nothing memoized again because last time it was called with 1 ...
};
const List = ({ ids }) => (
  <ul>
    {ids.map((id) => (
      <Item key={id} id={id} />
    ))}
  </ul>
);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM