Reputation: 3498
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
);
Upvotes: 7
Views: 11992
Reputation: 42218
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.
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.
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.
Upvotes: 7
Reputation: 403
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]);
// ...
};
Upvotes: 0