Recoil: How to prevent re-rendering of component when selectorFamily state remains unchanged?

Created on 14 Aug 2020  路  5Comments  路  Source: facebookexperimental/Recoil

Hi,

Really love Recoil, and have started using it in a project quite extensively. I clearly see the potential of this state mgmt library.
I have setup an app with multiple atoms and selectors, and it all works very smooth, however, I have one scenario that I have yet to solve in a clever manner, and I have not been able to find more info about it on your docs.

I have a table with multiple rows. Each row is a React.memo sub-component that uses Recoil hooks. The Recoil part of the component looks as follows (excuse my variable namings):

const setMarkInvalidModalState = useSetRecoilState(markInvalidModalState)
const setMarkInvalidSelectTrade = useSetRecoilState(markInvalidSelectTradeState)

const setOpenTradeModal = useSetRecoilState(openTradeModalState)
const setOpenTradeSelectTrade = useSetRecoilState(openTradeSelectTradeState)

const markForTrade = useRecoilValue(getMarkForTrade(compositeKey));

The only place i subscribe for updates and re-renderings is useRecoilValue of getMarkForTrade. getMarkForTrade is a selectorFamily, that looks as follows:

const getMarkForTrade = selectorFamily({
  key: 'MarkForTradeState',
  get: (compositeKey: string) => ({get}) => {
    const allMarks = get(getTradeMarks);

    const mark = allMarks[compositeKey];
    return ( mark !== undefined ? mark : null) as TradeMark | null;
  },
});

Now, whenever the underlying selector getTradeMarks (called in getMarkForTrade) is updated, all components subscribed to getMarkForTrade is also getting re-rendered. Even though the output / state of getMarkForTrade has remained unchanged for the input.

Basically all rows in the table are re-rendered every time, although 90% of the rows have the same output state from getMarkForTrade.

How can I prevent this? I have tested multiple times that this is the selector that triggers the re-render.

duplicate

Most helpful comment

I've been using this in the same situation, a table where only the row that was changed gets re-rendered.

export function useMemoizedRecoilValue<T>(recoilValue: RecoilValueReadOnly<T>): T {
  const [, forceRender] = React.useReducer((s: number) => s + 1, 0);
  const lastVal = React.useRef<T>();
  const lastRecoilValue = React.useRef<RecoilValueReadOnly<T>>();
  const init = useRecoilCallback(
    ({ snapshot }) => () => {
      lastVal.current = snapshot.getLoadable(recoilValue).contents as T;
      lastRecoilValue.current = recoilValue;
    },
    [recoilValue]
  );
  useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
    const cur = snapshot.getLoadable(recoilValue).contents as T;
    if (cur !== lastVal.current || lastRecoilValue.current !== recoilValue) {
      lastVal.current = cur;
      forceRender();
    }
  });

  if (lastRecoilValue.current !== recoilValue) {
    init();
  }

  return lastVal.current as T;
}

Doesn't work with promises and just uses !== to compare, but both could be supported by simple changes to the code.

In my opinion, the best built-in solution would be that the selector is passed the old value to the get function and the selector can implement any checks or whatnot itself. You can see in #314 that it is hard to implement a generic method of suppressing updates so my idea is:

  • selector is passed the old value
  • if the selector returns the exact same javascript object passed to old, downstream updates are suppressed. This is just like the recent change to atoms which suppressed updates with the exact same object is set.

This way selectors that don't care just ignore the old input and the few selectors which need to suppress downstream updates can implement checks at the beginning of the function.

All 5 comments

Could you provide the minimal reproducible sample?

I've been using this in the same situation, a table where only the row that was changed gets re-rendered.

export function useMemoizedRecoilValue<T>(recoilValue: RecoilValueReadOnly<T>): T {
  const [, forceRender] = React.useReducer((s: number) => s + 1, 0);
  const lastVal = React.useRef<T>();
  const lastRecoilValue = React.useRef<RecoilValueReadOnly<T>>();
  const init = useRecoilCallback(
    ({ snapshot }) => () => {
      lastVal.current = snapshot.getLoadable(recoilValue).contents as T;
      lastRecoilValue.current = recoilValue;
    },
    [recoilValue]
  );
  useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
    const cur = snapshot.getLoadable(recoilValue).contents as T;
    if (cur !== lastVal.current || lastRecoilValue.current !== recoilValue) {
      lastVal.current = cur;
      forceRender();
    }
  });

  if (lastRecoilValue.current !== recoilValue) {
    init();
  }

  return lastVal.current as T;
}

Doesn't work with promises and just uses !== to compare, but both could be supported by simple changes to the code.

In my opinion, the best built-in solution would be that the selector is passed the old value to the get function and the selector can implement any checks or whatnot itself. You can see in #314 that it is hard to implement a generic method of suppressing updates so my idea is:

  • selector is passed the old value
  • if the selector returns the exact same javascript object passed to old, downstream updates are suppressed. This is just like the recent change to atoms which suppressed updates with the exact same object is set.

This way selectors that don't care just ignore the old input and the few selectors which need to suppress downstream updates can implement checks at the beginning of the function.

Duplicate of #314

@wuzzeb - See optimization in #749

Was this page helpful?
0 / 5 - 0 ratings