Redux + Hooks useDispatch() in useEffect calling action twice - javascript

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

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);

Related

How can i test custom fetch hook

I am struggling with an issue with the custom fetch hook.Simply i am trying to test my fetch hook if the data already fetched the hook needs to get data from cache instead of api.
The test case fails and looks like caching mechanism not working, but if i try on the browser with manual prop change caching mechanism works properly.
import { render, waitFor } from "#testing-library/react";
const renderList = (filterParams = testFilterParamsList[0]) =>
render(<List filterParams={filterParams} />);
it("should re-render without fetch", async () => {
const { rerender } = renderList(testFilterParamsList[0]);
rerender(<List filterParams={testFilterParamsList[1]} />);
expect(window.fetch).toHaveBeenCalledTimes(1);
});
// useFetch.js
import {useEffect, useReducer} from "react";
const cache = {};
const FETCH_REQUEST = "FETCH_REQUEST";
const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_ERROR = "FETCH_SUCCESS";
const INITIAL_STATE = {
isPending: false,
error: null,
data: [],
};
const useFetch = ({url, filterOptions}) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case FETCH_REQUEST:return {...INITIAL_STATE, isPending: true};
case FETCH_SUCCESS: return {...INITIAL_STATE, isPending: false, data: action.payload};
case FETCH_ERROR: return {...INITIAL_STATE, isPending: false, error: action.payload};
default: return state;
}
}, INITIAL_STATE);
useEffect(() => {
const fetchData = async () => {
dispatch({type: FETCH_REQUEST});
if (cache[url]) {
const data = cache[url];
dispatch({type: FETCH_SUCCESS, payload: data});
} else {
try {
const response = await window.fetch(url);
let data = await response.json();
cache[url] = data
dispatch({type: FETCH_SUCCESS, payload: data});
} catch (err) {
dispatch({type: FETCH_ERROR, payload: err});
}
}
};
fetchData();
}, [filterOptions, url]);
return state;
};
export default useFetch;
// List.js
import useFetch from "../hooks/useFetch";
export const RocketsList = ({ filterParams }) => {
const { isPending, error, data } = useFetch({
url: "https://api.spacexdata.com/v3/launches/past",
name:filterParams.name,
});
return (
<div>
Doesn't matter
</div>
);
};

React | Redux Toolkit Shared State Undefined

I am creating a simple app to play with Redux ToolKit along react; however, I can see the object in the redux chrome tab, but unable to access it through components using react hooks.
My slice:
import {
createSlice,
createSelector,
createAsyncThunk,
} from "#reduxjs/toolkit";
const initialState = {
cryptoList: [],
loading: false,
hasErrors: false,
};
export const getCryptos = createAsyncThunk("cryptos/get", async (thunkAPI) => {
try {
const cryptosUrl =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd";
let response = await fetch(cryptosUrl);
console.log(response);
return await response.json();
} catch (error) {
console.log(error);
}
});
const cryptoSlice = createSlice({
name: "cryptos",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getCryptos.pending, (state) => {
state.loading = true;
});
builder.addCase(getCryptos.fulfilled, (state, { payload }) => {
state.loading = false;
state.cryptoList = payload;
});
builder.addCase(getCryptos.rejected, (state, action) => {
state.loading = false;
state.hasErrors = action.error.message;
});
},
});
export const selectCryptos = createSelector(
(state) => ({
cryptoList: state.cryptoList,
loading: state.loading,
}),
(state) => state
);
export default cryptoSlice;
My component:
import React, { useEffect } from "react";
import { getCryptos, selectCryptos } from "./cryptoSlice";
import { useSelector, useDispatch } from "react-redux";
const CryptoComponent = () => {
const dispatch = useDispatch();
const { cryptoList, loading, hasErrors } = useSelector(selectCryptos);
useEffect(() => {
dispatch(getCryptos());
}, [dispatch]);
const renderCrypto = () => {
if (loading) return <p>Loading Crypto...</p>;
if (hasErrors) return <p>Error loading news...</p>;
if (cryptoList) {
return cryptoList.data.map((crypto) => <p> {crypto.id}</p>);
}
};
return (
<div className="container">
<div className="row">CryptoComponent: {renderCrypto()}</div>
</div>
);
};
export default CryptoComponent;
All constructed values from the state: cryptoList, loading, hasErrors, seem to be undefined at the component level.
Any suggestions are appreciated!
Have you tried using the following createSelector code:
export const selectCryptos = createSelector(
(state) => state,
(state) => ({
cryptoList: state.cryptoList,
loading: state.loading,
})
);
As per documentation state should be the first parameter:
https://redux.js.org/usage/deriving-data-selectors

react/redux fetching data from redux slice issue

I am creating a react/ redux app using json fake api server redux toolkit.
I am adding a some datas from and api to redux slice and trying to retrieve to my component. data seems empty and no error showing could anyone able to help me in this if possible.
my redux slice
import {
createAsyncThunk,
createSlice,
createSelector,
} from "#reduxjs/toolkit";
import axios from "axios";
import { base_emp } from "./api";
const initialState = {
emplist: [],
loading: "loading",
};
export const fetchEmployees = createAsyncThunk(
"employee/emplist",
async (_, thunkAPI) => {
try {
const response = await axios.get(base_emp);
return await response.json();
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
}
);
export const userSlice = createSlice({
name: "user",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchEmployees.pending, (state) => {
state.emplist = [];
state.loading = "loading";
});
builder.addCase(fetchEmployees.fulfilled, (state, { payload }) => {
state.emplist = payload;
state.loading = "loaded";
});
builder.addCase(fetchEmployees.rejected, (state, action) => {
state.loading = "error";
state.error = action.error.message;
});
},
});
export const { increment, decrement, incrementByAmount } = userSlice.actions;
export const selectCount = (state) => state.counter.value;
export const selectEmployees = createSelector(
(state) => ({
products: state.emplist,
loading: state.loading,
}),
(state) => state
);
export default userSlice.reducer;
my component goes here
import React, { useState, useEffect } from "react";
import EmployeeDetails from "./EmployeeDetails";
import { useSelector, useDispatch } from "react-redux";
import { fetchEmployees, selectEmployees } from "./features/auth/userSlice";
const Employelist = () => {
const [employe, setEmployee] = useState([]);
const dispatch = useDispatch();
const { emplist } = useSelector(selectEmployees);
React.useEffect(() => {
dispatch(fetchEmployees());
}, [dispatch]);
useEffect(() => {
emplist &&
emplist.then((res) => res.json()).then((data) => setEmployee(data));
}, []);
const handleclick = (id) => {
const emp = employe.filter((emp) => emp.employeeid !== id);
setEmployee(emp);
};
// console.log(currentUsers);
return (
<div>
<EmployeeDetails handleclick={handleclick} employe={employe} />
</div>
);
};
export default Employelist;
api.js
import axios from "axios";
export const base_emp = axios.get("http://localhost:5000/emp");
error message in console

TypeError: updateElement is not a function

I am trying to update an element from an array by adding an object as a property like shown in this picture
When a user clicks on a single node button, a modal appears the user fills the form and then it is addes as a property for this node.
But for some reason I get this type error that says that the updateElement is not a function.
BTW, I am using Redux & react-flow-renderer libraries.
Reducer
import * as types from '../actions/types';
const initialState = {
elements: []
};
const flow = (state = initialState, action) => {
switch (action.type) {
case types.UPDATE_ELEMENT:
return {
...state,
elements: state.elements.map((e) => {
if (e.id === action.payload.id) {
e = {
...e,
options: action.payload.options,
};
}
return e;
}),
};
default:
return state;
}
};
export default flow;
Action
import { UPDATE_ELEMENT } from './types';
export const updateElement = (data) => (dispatch) => {
dispatch({
type: UPDATE_ELEMENT,
payload: data,
});
};
Node modal
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { updateElement } from '../../../../redux/actions/flow';
const VPCNodeModal = (props, { updateElement }) => {
const [formData, setFormData] = useState({
instance: '',
});
// options
const { instance } = formData;
const onFormChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmitForm = () => {
const update = {
id: selectedElement.id,
options: formData,
};
updateElement(update);
};
return (
<>
<Modal {...props}>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitForm();
}}
>
<label>
<span> Instance name:</span>
<input
type='text'
name='instance'
value={instance}
onChange={onFormChange}
/>
</label>
<button type='submit'>Submit</button>
</form>
</Modal>
</>
);
};
VPCNodeModal.propTypes = {
updateElement: PropTypes.func.isRequired
};
export default connect(null, { updateElement })(VPCNodeModal);
Issue is while receiving the props.
change
const VPCNodeModal = (props, { updateElement }) => {
to
const VPCNodeModal = (props) => {
const { updateElement } = props;
updateElement is a props was passes in VPCNodeModal. So you should update like this with spread operator
const VPCNodeModal = ({ updateElement, ...props }) => {

React-Redux: Action is Dispatched but Reducer is not updating the state

I'm trying to check if my store is onboarded or not. for that, I'm making an API call through the redux to check it in the BE and if it's true I'll redirect it to the dashboard. I'm able to get the data successfully from BE, and on success checkIsStoreOnboardedSuccess() is called but in the reducer, the state is not updated with the CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS state in the reducer.
action.js
import * as actionTypes from './index';
import API from '../../api';
export const clearCheckIsStoreOnboarded = () => {
return {
type: actionTypes.CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING,
};
};
export const checkIsStoreOnboarded = (payload) => {
return (dispatch) => {
dispatch(checkIsStoreOnboardedInitiate());
API.getAccountSettings(payload)
.then((response) => {
checkIsStoreOnboardedSuccess(response.data);
})
.catch((err) => {
checkIsStoreOnboardedFailure(err);
});
};
};
const checkIsStoreOnboardedInitiate = () => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START,
};
};
const checkIsStoreOnboardedSuccess = (data) => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS,
data: data,
};
};
const checkIsStoreOnboardedFailure = (err) => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL,
data: err,
};
};
reducer.js
import * as actionTypes from '../actions';
const initialState = {
isLoading: true,
isError: false,
isDone: false,
data: [],
error: null,
};
const clearCheckIsStoreOnboarded = () => {
return initialState;
};
const checkIsStoreOnboardedStart = (state) => {
return { ...state, isLoading: true, error: null, isError: false };
};
const checkIsStoreOnboardedSuccess = (state, action) => {
return { ...state, data: action.data, isDone: true, isLoading: false };
};
const checkIsStoreOnboardedFailure = (state, action) => {
return { ...state, error: action.data, isLoading: false, isError: true };
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING:
return clearCheckIsStoreOnboarded();
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START:
return checkIsStoreOnboardedStart(state);
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS:
return checkIsStoreOnboardedSuccess(state, action);
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL:
return checkIsStoreOnboardedFailure(state, action);
default:
return state;
}
};
export default reducer;
actionTypes.js
export const CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING = 'CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL';
onboard.js
import React, { useState, useEffect } from 'react';
import { withCookies } from 'react-cookie';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import Crew from './Crew';
import Service from './Services';
import Address from './Address';
import { useStyles } from './css/index.css';
import Header from './header';
import Stepper from './stepper';
import { getStoreID } from '../../../utils';
import {
clearCheckIsStoreOnboarded,
checkIsStoreOnboarded,
} from '../../../store/actions/check-is-store-onboarded-for-onboarding'
import Loader from '../../../components/CircularProgressLoader';
const OnboardScreen = ({
cookies,
clearCheckIsStoreOnboarded,
checkIsStoreOnboarded,
checkIsStoreOnboardedData,
}) => {
const [step, setStep] = useState(0);
// eslint-disable-next-line no-unused-vars
const [width, isDesktop] = useWindowWitdh();
const classes = useStyles(isDesktop);
const store_id = getStoreID(cookies);
useEffect(() => {
checkIsStoreOnboarded({
store_id,
});
}, []);
useEffect(() => () => clearCheckIsStoreOnboarded(), []);
if(checkIsStoreOnboarded.isDone){
<Redirect to='/dashboard'>
}
const updateStep = () => {
const updatedStep = step + 1;
setStep(updatedStep);
};
const onboardingScreenToRender = () => {
switch (step) {
case 0:
return (
<Crew />
);
case 1:
return (
<Service />
);
case 2:
return <Address />;
}
};
return (
<div className={classes.container}>
<Header isDesktop={isDesktop} />
<div className={classes.contentOfContainer}>
<div className={classes.titleHeader}>
Onboarding
</div>
<Stepper stepNumber={step} setStepNumber={setStep} />
{checkIsStoreOnboardedData.isLoading && <Loader />}
</div>
</div>
// <OnboardLoader />
);
};
const mapStateToProps = (state, ownProps) => {
return {
...ownProps,
checkIsStoreOnboardedData: state.checkIsStoreOnboardedForOnboardingReducer
};
};
const mapDispatchToProps = (dispatch) => {
return {
checkIsStoreOnboarded: (payload) => dispatch(checkIsStoreOnboarded(payload)),
clearCheckIsStoreOnboarded: () => dispatch(clearCheckIsStoreOnboarded()),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withCookies(OnboardScreen));
You need to dispatch your actions:
export const checkIsStoreOnboarded = (payload) => {
return (dispatch) => {
dispatch(checkIsStoreOnboardedInitiate());
API.getAccountSettings(payload)
.then((response) => {
// here
dispatch(checkIsStoreOnboardedSuccess(response.data));
})
.catch((err) => {
// and here
dispatch(checkIsStoreOnboardedFailure(err)(;
});
};
};
That said: you are writing a very outdated style of Redux here - in modern Redux, all of that would probably be possible with 1/4 of the code. If you are just learning Redux, you are probably following a very outdated tutorial. Modern Redux does not require you to write action type strings or action creators and your reducers can contain mutable logic. Also, it does not use connect unless you are working with legacy class components (which you don't seem to be doing).
I really recommend you to read the official Redux tutorial at https://redux.js.org/tutorials/essentials/part-1-overview-concepts

Categories

Resources