How can I memoize a variable derived from redux useSelector?
Asked Answered
Y

2

7

How can I memoize my rawTranscript variable so it doesn't trigger the useEffect below which subsequently triggers the expensive transcriptParser function? I've been trying a lot of different approaches, but the fact that I am using a redux-hook (useAppSelector) to capture the data from the store means I cannot use an empty dependency useEffect for the initial mount (hooks can't be inside of useEffect). I also can't seem to wrap the useAppSelector with a useMemo either for the same reason.

Any thought's on how I can memoize the rawTranscript variable so it doesn't re-trigger the useEffect?

error when using the redux-hook inside useMemo, useEffect, useCallback:

React Hook "useAppSelector" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.

component

const TranscriptCardController = (): JSX.Element => {
  const dispatch = useAppDispatch();
  // how to memoize rawTranscript?
  const rawTranscript = useAppSelector(selectRawTranscript);
  const parsedTranscript = useAppSelector(selectParsedTranscript);

  useEffect(() => {
    const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
    dispatch(updateParsedTranscript(parsedResult));
  }, [dispatch, rawTranscript]);

  // ...
};

selector

export const selectRawTranscript = createSelector(
  (state: RootState) => state.transcript.rawTranscript,
  (rawTranscript): RawTranscript => rawTranscript
);
Yvetteyvon answered 22/8, 2021 at 14:2 Comment(0)
S
6

There is no issue here if your selectRawTranscript function is purely selecting a value from the store, like state => state.transcript.raw. Your effect will only run when the value of rawTranscript changes -- as it should.

If your selectRawTranscript function returns a new object every time (like it if it involves array mapping, etc.) then this is a problem that you can address either in the selector itself or in the component.


Memoized Selectors

The best place to fix this is by using createSelector to create a memoized selector. For example:

import {createSelector} from '@reduxjs/toolkit';

export const selectRawTranscript = createSelector(
  (state: RootState) => state.data.someRawValue,
  (rawValue) => rawValue.map(entry => entry.data)
);

The second part of the selector is the "combiner" and it will only re-run when the value selected in the first part changes. So you get a consistent object reference.


Equality Comparisons

If you want to fix this in the component, the way to do that is by including a second argument on useAppSelector (which I'm assuming is just a typed version of useSelector).

This second argument allows you to specify a custom equality function so that you have more control over when the selected data is considered to be "changed". It's common to use a shallow equality comparison, so this is actually included in the react-redux package.

import { shallowEqual } from 'react-redux';
import { useAppSelector } from ...

const TranscriptCardController = (): JSX.Element => {
  const rawTranscript = useAppSelector(selectRawTranscript, shallowEqual);
...

Note: it's impossible for me to know whether or not you really do have a problem with undesirable changes in rawTranscript because you haven't included your selector function. You might be overthinking this and it might be a non-issue.

Swarts answered 23/8, 2021 at 0:46 Comment(2)
Hello, thank you for the response! I've updated my original post to include the selector I used. If there is anything let me know.Yvetteyvon
I see you posted another question where Mark explained why createSelector doesn’t do anything here. You’re definitely overthinking :) Your original code is fine.Swarts
S
0

Create a standalone useCallback where your dispatch will run on every store update but useEffect will only execute when the callback method is executed.

const TranscriptCardController = (): JSX.Element => {
  const dispatch = useAppDispatch();
  // how to memoize rawTranscript?
  const rawTranscript = useAppSelector(selectRawTranscript);
  const parsedTranscript = useAppSelector(selectParsedTranscript);
  
  const callback = useCallback(() => {
    const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
    dispatch(updateParsedTranscript(parsedResult));
  }, [rawTranscript])


  useEffect(() => {
    const unsubscribe = callback()

    return unsubscribe
  }, [callback]);

  // ...
};
Subsonic answered 22/8, 2021 at 14:9 Comment(2)
thank you for the response, it's interesting. However, I feel like it's a bit confusing and thus hard to maintain and build on. That said, it did make me think about underlying problem I have and thinking on it more, perhaps memoization isn't really appropriate for the use-case. Exploring your example however, if we unsubscribe from the callback (when we unmount) when we remount it will re-trigger the initial useCallback because rawTranscript will still be seen as a new value.Yvetteyvon
You don't have to use unsubscribe, I just put it for async callback because it can cause memory leak problems, but useEffect will only re-trigger if useCallback is triggered again.Chamfron

© 2022 - 2024 — McMap. All rights reserved.