Search code examples
javascriptreactjsreduxreact-reduxreact-hooks

Redux + Hooks useDispatch() in useEffect calling action twice


I'm beginner in redux & hooks. I am working on form handling and trying to call an action through useDispatch hooks but it is calling my action twice.

I'm referring this article.

Here is the example:

useProfileForm.js

import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchProfile } from '../../../redux/profile/profile.actions';

const useProfileForm = (callback) => {

    const profileData = useSelector(state =>
        state.profile.items        
    );

    let data;
    if (profileData.profile) {
        data = profileData.profile;
    }

    const [values, setValues] = useState(data);

    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(fetchProfile());
    }, [dispatch]);

    const handleSubmit = (event) => {
        if (event) {
            event.preventDefault();
        }
        callback();
    };

    const handleChange = (event) => {
        event.persist();
        setValues(values => ({ ...values, [event.target.name]: event.target.value }));
    };

    return {
        handleChange,
        handleSubmit,
        values,
    }
};

export default useProfileForm;

Action

export const FETCH_PROFILE_BEGIN = "FETCH_PROFILE_BEGIN";
export const FETCH_PROFILE_SUCCESS = "FETCH_PROFILE_SUCCESS";
export const FETCH_PROFILE_FAILURE = "FETCH_PROFILE_FAILURE";
export const ADD_PROFILE_DETAILS = "ADD_PROFILE_DETAILS";

function handleErrors(response) {
    if (!response.ok) {
        throw Error(response.statusText);
    }
    return response;
}

function getProfile() {
    return fetch("url")
        .then(handleErrors)
        .then(res => res.json());
}

export function fetchProfile() {
    return dispatch => {
        dispatch(fetchProfileBegin());
        return getProfile().then(json => {
            dispatch(fetchProfileSuccess(json));
            return json;
        }).catch(error =>
            dispatch(fetchProfileFailure(error))
        );
    };
}

export const fetchProfileBegin = () => ({
    type: FETCH_PROFILE_BEGIN
});

export const fetchProfileSuccess = profile => {
    return {
        type: FETCH_PROFILE_SUCCESS,
        payload: { profile }
    }
};

export const fetchProfileFailure = error => ({
    type: FETCH_PROFILE_FAILURE,
    payload: { error }
});

export const addProfileDetails = details => {
    return {
        type: ADD_PROFILE_DETAILS,
        payload: details
    }
};

Reducer:

    import { ADD_PROFILE_DETAILS, FETCH_PROFILE_BEGIN, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from './profile.actions';

    const INITIAL_STATE = {
        items: [],
        loading: false,
        error: null
    };

    const profileReducer = (state = INITIAL_STATE, action) => {
        switch (action.type) {
            case ADD_PROFILE_DETAILS:
                return {
                    ...state,
                    addProfileDetails: action.payload
                }
            case FETCH_PROFILE_BEGIN:
                return {
                    ...state,
                    loading: true,
                    error: null
                };

            case FETCH_PROFILE_SUCCESS:
                return {
                    ...state,
                    loading: false,
                    items: action.payload.profile
                };

            case FETCH_PROFILE_FAILURE:
                return {
                    ...state,
                    loading: false,
                    error: action.payload.error,
                    items: []
                };
            default:
                return state;
        }
    }

    export default profileReducer;


**Component:**

import React from 'react';
import { connect } from 'react-redux';
import useProfileForm from './useProfileForm';
import { addProfileDetails } from '../../../redux/profile/profile.actions';

const EducationalDetails = () => {

    const { values, handleChange, handleSubmit } = useProfileForm(submitForm);

    console.log("values", values);


    function submitForm() {
        addProfileDetails(values);
    }

    if (values) {
        if (values.error) {
            return <div>Error! {values.error.message}</div>;
        }

        if (values.loading) {
            return <div>Loading...</div>;
        }
    }

    return (
        <Card>
            ...some big html
        </Card>
    )
}

const mapDispatchToProps = dispatch => ({
    addProfileDetails: details => dispatch(details)
});

export default connect(null, mapDispatchToProps)(EducationalDetails);

Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.

const { values, handleChange, handleSubmit } = useProfileForm(submitForm);

values is undefined

enter image description here

enter image description here


Solution

  • The twice dispatch of action is probably because you have used React.StrictMode in your react hierarchy.

    According to the react docs, in order to detect unexpected sideEffects, react invokes a certain functions twice such as

    Functions passed to useState, useMemo, or useReducer
    

    Now since react-redux is implemented on top of react APIs, actions are infact invoked twice

    Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.

    To answer this question, you must know that values is not the result coming from the response of dispatch action from reducer but a state that is updated when handleChange is called so that is supposed to remain unaffected by the action

    I think you mean to expose the redux data from useProfileForm which forgot to do

    const useProfileForm = (callback) => {
    
        const profileData = useSelector(state =>
            state.profile.items        
        );
    
        let data;
        if (profileData.profile) {
            data = profileData.profile;
        }
    
        const [values, setValues] = useState(data);
    
        const dispatch = useDispatch();
    
        useEffect(() => {
            dispatch(fetchProfile());
        }, [dispatch]);
    
        const handleSubmit = (event) => {
            if (event) {
                event.preventDefault();
            }
            callback();
        };
    
        const handleChange = (event) => {
            event.persist();
            setValues(values => ({ ...values, [event.target.name]: event.target.value }));
        };
    
        return {
            handleChange,
            handleSubmit,
            values,
            data // This is the data coming from redux store on FetchProfile and needs to logged
        }
    };
    
    export default useProfileForm;
    

    You can use the data in your component like

    const { values, handleChange, handleSubmit, data } = useProfileForm(submitForm);