React functional component execute multiple times, with useSelector() is executed multiple times in each function component execution

Viewed 1760

I've read through documents about hooks but don't see enough detailed information about the lifecycle of components when working with hooks, hence I need your help.

To be more specific, I have a sample code with the following related questions: https://github.com/khoitnm/practice-react-redux/tree/main/pro-00-old-react-version/src/comp-04-clickButton

Question 1) My functional component uses 3 hooks: useSelector, useDispatch, useEffect, but then why the component is executed 4 times (detail in the code and screenshot below)? From my understanding, it should be only 3 times:

  1. Initiation
  2. The useSelector triggers this execution
  3. The useEffect triggers this execution. The useDispatch will not trigger any additional execution, then why the data show 4 times?

Below are my code and screenshot. The component:

let componentCount = 0;
let returnCount = 0;
let useSelectorCount = 0;
let useEffectCount = 0;

const Comp04ClickButtonPage = (): JSX.Element => {
    componentCount++;
    console.log(`[${componentCount}] component - START ----------------------`);

    // useSelector
    const stringValueState = useSelector((rootState: RootState): string => {
        useSelectorCount++;
        console.log(`[${componentCount}] selectorCount: ${useSelectorCount}`);
        const result = rootState.comp04ClickButtonSlice.stringValue;
        return result;
    });
    console.log(`[${componentCount}] component - after useSelectorCount`);

    // useEffect
    const dispatch = useDispatch();
    useEffect(() => {
        useEffectCount++;
        console.log(`[${componentCount}] effectCount: ${useEffectCount}.`);
        dispatch(thunkComp04ClickButton(`${new Date().getTime()}`));
    }, [dispatch]);
    console.log(`[${componentCount}] component - after useEffect`);

    const onClickButton = () => {
        componentCount = returnCount = useSelectorCount = useEffectCount = 0; // reset counts
        console.log(`[${componentCount}] onClickNewValue`);
        dispatch(thunkComp04ClickButton(`Random ${new Date().getTime()}`));
    };

    // render
    returnCount++;
    return (
        <div>
            {console.log(`[${componentCount}] renderCount: ${returnCount}`)}
            <div>
                [{componentCount}], useEffect: {useEffectCount}, useSelectorCount: {useSelectorCount}, returnCount: {returnCount}
            </div>
            <button onClick={onClickButton}>Click Button</button>
        </div>
    );
}

The thunk:

export const thunkComp04ClickButton = (stringValue: string): AppThunk => async (dispatch) => {
    try {
        console.log(`AppThunk: saveStringValue "${stringValue}" start dispatch`);
        dispatch(setComp04ClickButtonState({ stringValue: stringValue }));
        console.log(`AppThunk: saveStringValue "${stringValue}" end dispatch`);
    } catch (err) {
        console.error(`AppThunk: saveStringValue error dispatch` + err, err);
    }
};

The slice:

type Comp04ClickButtonState = {
    stringValue: string;
};

const initialState: Comp04ClickButtonState = {
    stringValue: 'Init Comp04ClickButtonState',
};

const comp04ClickButtonSlice = createSlice({
    name: 'comp04ClickButtonSlice',
    initialState,
    reducers: {
        setComp04ClickButtonState(state, action: PayloadAction<Comp04ClickButtonState>) {
            console.log(`Slice: setComp04ClickButtonState "${action.payload.stringValue}"`);
            return action.payload;
        },
    },
});

export const { setComp04ClickButtonState } = comp04ClickButtonSlice.actions;
export default comp04ClickButtonSlice.reducer;

In the screenshot 1: you'll see that the componentCount is [4] and returnCount: 4

Question 2) When the component is triggered, why the useSelector is executed multiple times?

In more detail, we can see in the console log: In the second execution of the component, the selectorCount increase from 2 to 5 times (totally 4 times)

[2] selectorCount: 2
[2] component - after useSelectorCount
[2] component - after useEffect
[2] renderCount: 2
[2] selectorCount: 3
[2] effectCount: 1.
AppThunk: saveStringValue "1610983429863" start dispatch
Slice: setComp04ClickButtonState "1610983429863"
[2] selectorCount: 4
AppThunk: saveStringValue "1610983429863" end dispatch
[2] selectorCount: 5

In summary, when I open that web page, the final result is:

  • useEffect: 1 (as expected)
  • componentCount = returnCount: 4 (many times)
  • useSelector: 7 (why so many times???)

UPDATED: As @lawrence-witt mentioned, the <React.StrictMode> causes double-invokes on various things, so I change my index.tsx to use <React.Fragment> like this:

const render = () => {
    ReactDOM.render(
        <React.Fragment>
            <Provider store={store}>
                <App />
            </Provider>
        </React.Fragment>,
        document.getElementById('root')
    );
};

As the result, the component now is executed only 2 times. However, as you see in the screenshot 2, the console log still shows that the useSelector is triggered 4 times when the component is executed the first time. Hence the question 2 still remains: Why is it triggered so many times?

[1] component - START ----------------------
[1] selectorCount: 1
[1] component - after useSelectorCount
[1] component - after useEffect
[1] renderCount: 1
[1] selectorCount: 2
[1] effectCount: 1.
AppThunk: saveStringValue "1611023432655" start dispatch
Slice: setComp04ClickButtonState "1611023432655"
[1] selectorCount: 3
AppThunk: saveStringValue "1611023432655" end dispatch
[1] selectorCount: 4
1 Answers

The issue here is that when the useSelector hook is used, it updates the component every time the Redux-managed value changes. You can read more about useSelector here: https://react-redux.js.org/api/hooks#useselector.

The dependency you added to useEffect can be removed as dispatch never changes. The official documentation for useDispatch can be found here: https://react-redux.js.org/api/hooks#usedispatch.

// This will be called only once when the component is mounted for the first time
useEffect(() => {
   initDispatch();
},[]);
const initDispatch = useCallback(()=>{
  useEffectCount++;
  console.log(`[${componentCount}] effectCount: ${useEffectCount}.`);
  dispatch(thunkComp04ClickButton(`${new Date().getTime()}`));
}, [dispatch]);

This code will still re-render the component when dispatch is called (because dispatch is in the useCallback dependency array) which will force useSelector to re-render the component.

Related