0

I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.

button.tsx

interface Props {
    isActive: boolean;
    onClick: () => void;
}

const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
    useEffect(() => {
        console.log('RERENDER');
    }, []);

    return (
        <S.Button onClick={onClick} isActive={isActive}>
            {isActive ? 'Daily stats' : 'All time stats'}
        </S.Button>
    );
};

export default React.memo(StatsButton);

parent.tsx

const DashboardPage: React.FC = () => {

const {
        fetchDailyData,
        fetchAllTimeData,
    } = useDashboard();

    useEffect(() => {
        fetchCountry();
        fetchAllTimeData();
        // eslint-disable-next-line
    }, []);

    const handleClick = useEventCallback(() => {
        if (!statsButtonActive) {
            fetchDailyData();
        } else {
            fetchAllTimeData();
        }
        setStatsButtonActive(!statsButtonActive);
    });

return (
  <S.Container>
            <S.Header>
                <StatsButton
                    onClick={handleClick}
                    isActive={statsButtonActive}
                />
            </S.Header>
</S.Container>
)

}

fetch functions are using useCallback

export const useDashboard = (): Readonly<DashboardOperators> => {
    const dispatch: any = useDispatch();

    const fetchAllTimeData = useCallback(() => {
        return dispatch(fetchAllTimeDataAction());
    }, [dispatch]);

    const fetchDailyData = useCallback(() => {
        return dispatch(fetchDailyDataAction());
    }, [dispatch]);

    return {
        fetchAllTimeData,
        fetchDailyData,
    } as const;
};
kakakakakakakk
  • 467
  • 10
  • 31
  • 2
    Along with useMemo you would want to wrap your onClick in a useCallback. Since it is a function, it will be created fresh on every render. React.memo does shallow comparison – Tushar Shahi Jul 06 '22 at 16:42
  • Did you realize that `console.log('RERENDER');` will run not just on _re-renders_ but also on initial renders? Just want to make sure you're not getting a false read that re-renders are actually happening. – Jacob Jul 06 '22 at 18:12

1 Answers1

5

You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.

Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.

Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.

Update:

It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.

function useEventCallback(fn) {
  const realFn = useRef(fn);

  useEffect(() => {
    realFn.current = fn;
  }, [fn]);

  return useMemo((...args) => {
    realFn.current(...args)
  }, []);
}

This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:

const handleClick = useEventCallback(() => {
  if (!statsButtonActive) {
    fetchDailyData();
  } else {
    fetchAllTimeData();
  }
  setStatsButtonActive(!statsButtonActive);
});
Jacob
  • 77,566
  • 24
  • 149
  • 228
  • can you provide an example of `useCallback`? I have updated the parent component code – kakakakakakakk Jul 06 '22 at 16:50
  • I have updated the code using `useCallback` and it still rerenders – kakakakakakakk Jul 06 '22 at 16:57
  • You haven't shown where that function's _dependencies_ are declared. If the dependencies change then a new version of the function is returned and will thwart your efforts to memoize. Look into that `useEventCallback` alternative. That approach _always_ returns the same function that you can use with props but uses a ref to call a _real_ underlying function that is updated with any closed over variables without needing to specify a dependencies array. – Jacob Jul 06 '22 at 16:59
  • still does not work. It works fine fi I remove fetch functions. – kakakakakakakk Jul 06 '22 at 17:21
  • you can also check how `useDashboard` is implemented in the update – kakakakakakakk Jul 06 '22 at 17:22
  • also, if the parent does not rerender it means that the function is the same I assume. Which means that I done need to memoize the function? – kakakakakakakk Jul 06 '22 at 17:33