尽管自定义qualityFn检查返回true,选择器返回不同的值

时间:2021-03-15 17:23:48

标签: react-redux

我有以下选择器和效果

const filterValues = useSelector<State, string[]>(
    state => state.filters.filter(f => f.field === variableId).map(f => f.value),
    (left, right) => {
        return left.length === right.length && left.every(l => right.includes(l));
    },
);
const [value, setValue] = useState<SelectionRange>({ start: null, end: null });
useEffect(() => {
    const values = filterValues
            .filter(av => av).sort((v1, v2) => v1.localeCompare(v2));
        const newValue = {
            start: values[0] ?? null,
            end: values[1] ?? null,
        };
        setValue(newValue);
}, [filterValues]);

上面的选择器最初返回一个空数组,但每次都返回一个不同的数组,我不明白为什么,因为相等函数应该保证它不会。

这使得效果触发,设置状态,选择器再次运行(正常)但返回另一个不同的空数组!导致代码无限循环运行。 为什么选择器每次都返回不同的数组?我错过了什么? 我正在使用 react-redux 7.2.2

2 个答案:

答案 0 :(得分:1)

如果选择器是一个新的引用,react-redux 会电子运行选择器,因为它假设代码可能已经完全改变了它选择的内容

https://github.com/reduxjs/react-redux/issues/1654

一种解决方案是记住选择器函数

const selector = useMemo(() => (state: State) => state.filters.filter(f => f.field === variableId).map(f => f.value), [variableId]);
const filterValues = useSelector<State, string[]>(
    selector ,
    (left, right) => {
        return left.length === right.length && left.every(l => right.includes(l));
    },
);

答案 1 :(得分:0)

您可以尝试在选择器中记住过滤器的结果并在选择器中计算 value,现在我不确定您是否仍然需要 value 的本地状态,因为它只是来自 redux 状态的派生值的副本,并且只会在您复制它时导致额外的渲染,但代码如下:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const { useState, useEffect, useMemo } = React;

const initialState = {
  filters: [
    { field: 1, value: 1 },
    { field: 2, value: 2 },
    { field: 1, value: 3 },
    { field: 2, value: 4 },
  ],
};
//action types
const TOGGLE = 'NEW_STATE';
const NONE = 'NONE';
//action creators
const toggle = () => ({
  type: TOGGLE,
});
const none = () => ({ type: NONE });
const reducer = (state, { type }) => {
  if (type === TOGGLE) {
    return {
      filters: state.filters.map((f) =>
        f.field === 1
          ? { ...f, field: 2 }
          : { ...f, field: 1 }
      ),
    };
  }
  if (type === NONE) {
    //create filters again should re run selector
    //  but not re render
    return {
      filters: [...state.filters],
    };
  }
  return state;
};
//selectors
const selectFilters = (state) => state.filters;
const createSelectByVariableId = (variableId) => {
  const memoArray = defaultMemoize((...args) => args);
  return createSelector([selectFilters], (filters) =>
    memoArray.apply(
      null,
      filters
        .filter((f) => f.field === variableId)
        .map((f) => f.value)
    )
  );
};
const createSelectSelectValue = (variableId) =>
  createSelector(
    [createSelectByVariableId(variableId)],
    //?? does not work in SO because babel is too old
    (values) => ({
      start: values[0] || null,
      end: values[1] || null,
    })
  );
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
var last;
const App = ({ variableId }) => {
  const selectValue = useMemo(
    () => createSelectSelectValue(variableId),
    [variableId]
  );
  const reduxValue = useSelector(selectValue);
  if (last !== reduxValue) {
    console.log('not same', last, reduxValue);
    last = reduxValue;
  }
  //not sure if you still need this, you are just
  //  copying a value you already have
  const [value, setValue] = useState(reduxValue);
  const dispatch = useDispatch();
  useEffect(() => setValue(reduxValue), [reduxValue]);
  console.log('rendering...', value);
  return (
    <div>
      <button onClick={() => dispatch(toggle())}>
        toggle
      </button>
      <button onClick={() => dispatch(none())}>none</button>
      <pre>{JSON.stringify(value, undefined, 2)}</pre>
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App variableId={1} />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>