cart.js
const Cart = () => {
const cartListing = useSelector(state => state.cartList);
const {cart, loading, error} = cartListing;
const dispatch = useDispatch();
const removeFromCartHandler = (id) => {
dispatch(removeFromCart(id))
}; // =======>>>>> This function needs to dispatch on onclick event but it fire on first time load and does not fire on click
useEffect(() => {
dispatch(cartList());
return () => {
}
},[]);
return(
<button type="button" className="button" onClick={removeFromCartHandler()} >Delete </button>
action.js
const cartList = () => async (dispatch) => {
try {
dispatch({ type: CART_LIST_REQUEST });
const { data } = await axios.get('http://localhost:3001/cart');
dispatch({ type: CART_LIST_SUCCESS, payload: data.data.cartRecords });
}
catch (error) {
dispatch({ type: CART_LIST_FAIL, payload: error.message });
}
};
const removeFromCart = (id) => async (dispatch) => {
try {
dispatch({ type: CART_REMOVE_ITEM, payload: id });
const { data } = await axios.delete(`http://localhost:3001/cart/${id}`);
dispatch({ type: CART_REMOVE_SUCCESS, payload: data });
}
catch (error) {
dispatch({type: CART_REMOVE_FAIL, payload: error.message})
}
};
export { cartList, removeFromCart };
I don't know why removeFromCartHandler() is firing on first time loading, not on click. I want that removeFromCartHandler() this function fire on onclick event not on the first-time load as I have also not added in useEffect hook cartList is fine dispatching on first time but removeFromCartHandler() function need to dispatch on onClick event. I think I am missing some concept of hooks here. I am new to react-redux and stucked here. Any help will be appricated
your onClick handler is wrong . Do it like this
onClick={() => removeFromCartHandler()}
It's being called because you are calling it - placing () after removeFromCartHandler invokes the function.
onClick={removeFromCartHandler()}
You need to add more code for better context but what you have to do is something like this
onClick={(e) => removeFromCartHandler(e, item.id)}
your main function expects to get id you have to pass it over somehow.
I presume that you render cart items or do something similar, anyhow with this implementation proper id will not be passed.
Related
I am using React JS, Redux, React-
Redux
, Redux-Thunk.
File: Acions.js:
Here are my Redux Action functions:
export function setSuccess(data){
return{
type: "SET_SUCCESS",
payload: data
};
}
export function setMessage(data){
return{
type: "SET_POSTS",
payload: data
};
}
export const listMyBlogPosts = (userID, abortController) =>{
return (dispatch) =>{
axios.get(`http://locahost:5000/timeline/posts/${userID}`, {signal: abortController})
.then((response) =>{
dispatch(setSuccess(true));
dispatch(setMessage(response.data.message));
}).catch((error) =>{
console.log("err: ", error);
dispatch(setSuccess(false));
dispatch(setMessage(`${error}`));
});
}
};
Here is where I am using the above function:
File: ABC.js:
function ABC({ someCheck }){
const abortController = useRef(new AbortController());
const dispatch = useDispatch();
console.log(“ABC component”);
useEffect(() =>{
navigation.addListener("focus", () =>{
dispatch(listMyBlogPosts(userID, abortController.current.signal));
console.log("focus is being run!");
});
return () => abortController.current.abort();
}, []);
}
In the ABC component, I get the following error:
If I remove the useEffect hook, I do not get the above error ?
How to fix this error ?
Your code is adding more and more event 'focus' listeners, which is leading to lots of request call. So you need to remove those event listener once component is unmounted.
useEffect(() => {
const onFocus = () => {
dispatch(listMyBlogPosts(userID, abortController.current.signal))
console.log('focus is being run!')
}
navigation.addListener('focus', onFocus)
return () => {
navigation.removeListener('focus', onFocus)
abortController.current.abort()
}
}, [])
I am dispatching action addProducts on every mount of the ProductList component whereas i want to dispatch the action one timeusing useEffect hook and store the data in the redux and then use it.
Below are my actions file and ProductList component file.
actions.js file
export const addProducts = () => async (dispatch) => {
let Products = await axios.get("https://api.npoint.io/2a4561b816e5b6d00894");
return dispatch({
type: ADD_PRODUCTS,
payload: Products.data,
});
};
ProductList.js component file
import { addProducts } from "../actions/Index";
const ProductList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(addProducts());
},[]);
const Products = useSelector((state) => state.products);
console.log(Products)
You could just dispatch the action in the component but in the thunk action do nothing if products are available:
export const addProducts = () => async (
dispatch,
getState,//thunk also get a getState function
) => {
//you should write a dedicated selector function so you could do:
// const productsInState = selectProducts(getState())
const productsInState = getState().products
//whatever would make state.products available
// reducers are missing in your question
if(productsInState){
//do nothing if products are already in state
return;
}
let Products = await axios.get(
'https://api.npoint.io/2a4561b816e5b6d00894',
);
return dispatch({
type: ADD_PRODUCTS,
payload: Products.data,
});
};
In your component you can just dispatch on each render, if your page has multiple components dispatching this action then you could make a grouped action.
You want to check if products are already in redux with an if(!products){...} e.g.
const addProducts = () => async (dispatch) => {
let Products = await axios.get("https://api.npoint.io/2a4561b816e5b6d00894");
return dispatch({
type: ADD_PRODUCTS,
payload: Products.data,
});
};
const ProductList = () => {
const dispatch = useDispatch();
const products = useSelector((state) => state.products);
useEffect(() => {
if (!products) {
dispatch(addProducts());
}
},[dispatch]);
return <p>foo</p>
}
export const addProducts = () => async dispatch => {
dispatch({
type: ADD_PRODUCTS_START,
payload: { loading: true },
});
const Products = await axios.get(
'https://api.npoint.io/2a4561b816e5b6d00894'
);
dispatch({
type: ADD_PRODUCTS_SUCCESS,
payload: { products: Products.data, loading: false },
});
};
const ProductList = ({ products, loading }) => {
useEffect(() => {
if (!products && !loading) {
dispatch(addProducts());
}
}, []);
};
const mapStateToProps = ({ products: { data, loading } }) => ({ products: data, loading });
I am trying to put a condition on my button with the help of my redux state, the code is as follows
{showotpbox == true ? (
<IonItem>
<IonLabel position="stacked">OTP</IonLabel>
<IonInput
type="number"
onIonChange={(e) => handleChangeOtp(String(e.detail.value))}
/>
</IonItem>
) : (
""
)}
where showotpbox is a react hook state, which initally is set to false, now after clicking a button that says "GET OTP" i dispatch an action
let data = { email };
await dispatch(requestOTP(data));
which then after succes dispatch a type that changes state of a reducer
export const requestOTP = (data: any) => (dispatch: any) => {
console.log('requesting')
dispatch({ type: "LOADING", payload: true });
console.log(data);
axios
.post("http://localhost:4000/request_otp", data)
.then((res) => {
console.log(res)
if (res.data.status == 200) {
dispatch({type:'SHOW_OTP_BOX'})
}
})
.catch((err) => {
dispatch({ type: "LOADING", payload: false });
dispatch(errorActions());
});
};
which in turn updates a state in my modal reducer
case 'SHOW_OTP_BOX':
return {...state,showotpbox:true}
but i see that action is somehow async cause when i look at this function
const modal = useSelector((state: RootState) => {
return {show: state.modal.showotpbox };
});
const getotp = async () => {
let data = { email };
await dispatch(requestOTP(data));
console.log(modal.show);
if (modal.show == true) {
setshowotpbox(true);
}
};
"console.log(modal.show);" still gives me false, although the state has been update to true and that's why my input box doesn't render
i try to use async await here in my getotp function to wait for the action to be completed but that doesn't work it seems
As dispatch is async you have to handle store updation as soon as it updates callback function will be called.
var store = createStore(); // Redux store if it is created it should return store else it should create store and return
var storeUnsubscribe = store.subscribe(handleStoreChange);
function handleStoreChange() {
console.log(state.modal.show);
if (state.modal.show == true) {
setshowotpbox(true);
}
}
const modal = useSelector((state: RootState) => {
return {show: state.modal.showotpbox };
});
const getotp = async () => {
let data = { email };
await dispatch(requestOTP(data));
};
I am attempting to query my Firebase backend through a redux-thunk action, however, when I do so in my initial render using useEffect(), I end up with this error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
My action simply returns a Firebase query snapshot which I then received in my reducer. I use a hook to dispatch my action:
export const useAnswersState = () => {
return {
answers: useSelector(state => selectAnswers(state)),
isAnswersLoading: useSelector(state => selectAnswersLoading(state))
}
}
export const useAnswersDispatch = () => {
const dispatch = useDispatch()
return {
// getAnswersData is a redux-thunk action that returns a firebase snapshot
setAnswers: questionID => dispatch(getAnswersData(questionID))
}
}
and the following selectors to get the data I need from my snapshot and redux states:
export const selectAnswers = state => {
const { snapshot } = state.root.answers
if (snapshot === null) return []
let answers = []
snapshot.docs.map(doc => {
answers.push(doc.data())
})
return answers
}
export const selectAnswersLoading = state => {
return state.root.answers.queryLoading || state.root.answers.snapshot === null
}
In my actual component, I then attempt to first query my backend by dispatching my action, and then I try reading the resulting data once the data is loaded as follows:
const params = useParams() // params.id is just an ID string
const { setAnswers, isAnswersLoading } = useAnswersDispatch()
const { answers } = useAnswersState()
useEffect(() => {
setAnswers(params.id)
}, [])
if (!isAnswersLoading)) console.log(answers)
So to clarify, I am using my useAnswersDispatch to dispatch a redux-thunk action which returns a firebase data snapshot. I then use my useAnswersState hook to access the data once it is loaded. I am trying to dispatch my query in the useEffect of my actual view component, and then display the data using my state hook.
However, when I attempt to print the value of answers, I get the error from above. I would greatly appreciate any help and would be happy to provide any more information if that would help at all, however, I have tested my reducer and the action itself, both of which are working as expected so I believe the problem lies in the files described above.
Try refactoring your action creator so that dispatch is called within the effect. You need to make dispatch dependent on the effect firing.
See related
const setAnswers = (params.id) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(useAnswersDispatch(params.id));
}, [])
}
AssuminggetAnswersData is a selector, the effect will trigger dispatch to your application state, and when you get your response back, your selector getAnswersData selects the fields you want.
I'm not sure where params.id is coming from, but your component is dependent on it to determine an answer from the application state.
After you trigger your dispatch, only the application state is updated, but not the component state. Setting a variable with useDispatch, you have variable reference to the dispatch function of your redux store in the lifecycle of the component.
To answer your question, if you want it to handle multiple dispatches, add params.id and dispatch into the dependencies array in your effect.
// Handle null or undefined param.id
const answers = (param.id) => getAnswersData(param.id);
const dispatch = useDispatch();
useEffect(() => {
if(params.id)
dispatch(useAnswersDispatch(params.id));
}, [params.id, dispatch]);
console.log(answers);
As commented; I think your actual code that infinite loops has a dependency on setAnswers. In your question you forgot to add this dependency but code below shows how you can prevent setAnswers to change and cause an infinite loop:
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
// const dispatch = useDispatch(); //react-redux useDispatch will never change
//never re create setAnswers because it causes the
// effect to run again since it is a dependency of your effect
const setAnswers = React.useCallback(
questionID => dispatch(getAnswersData(questionID)),
//your linter may complain because it doesn't know
// useDispatch always returns the same dispatch function
[dispatch]
);
return {
setAnswers,
};
};
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is your original code causing infinite loop because setAnswers keeps changing.
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
return {
//re creating setAnswers, calling this will cause
// state.data to be set causing Data to re render
// and because setAnser has changed it'll cause the
// effect to re run and setAnswers to be called ...
setAnswers: questionID =>
dispatch(getAnswersData(questionID)),
};
};
let timesRedered = 0;
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
//securit to prevent infinite loop
timesRedered++;
if (timesRedered > 20) {
throw new Error('infinite loop');
}
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You just need to add params.id as a dependency.
Don't dispatch inside the function which you are calling inside useEffect but call another useEffect to dispatch
const [yourData, setyourData] = useState({});
useEffect(() => {
GetYourData();
}, []);
useEffect(() => {
if (yourData) {
//call dispatch action
dispatch(setDatatoRedux(yourData));
}
}, [yourData]);
const GetYourData= () => {
fetch('https://reactnative.dev/movies.json')
.then((response) => response.json())
.then((json) => {
if (result?.success == 1) {
setyourData(result);
}
})
.catch((error) => {
console.error(error);
});
};
I have created a custom Hook which fetches data from the server, sends dispatch to the store and returns data. It is usable if I want to list all comments in my app, however, I wanted to reuse it in the component where I need to fetch all comment replies, and that should happen only when certain button is clicked.
This is the hook down below.
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
const useFetch = (url, options, actionType, dataType) => {
const [response, setResponse] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
(async () => {
const res = await fetch(url);
const json = await res.json();
setResponse(json);
})();
}, []);
useEffect(() => {
dispatch({ payload: response, type: actionType });
}, [response]);
const data = useSelector(state => state[dataType]);
return data;
};
export default useFetch;
Inside of my component I need to fetch replies when a button is clicked
const ParentComment = ({ comment }) => {
const handleShowMoreReplies = (e) => {
e.preventDefault();
}
let replies = useFetch(
`/api/comment_replies?comment_id=${comment.id}`,
null,
"REPLIES_RECEIVED",
"replies"
);
return (
<div>
<Comment comment={comment} />
<div className="replies">
{replies.map(reply => (
<Comment key={reply.id} comment={reply} />
))}
<a href="#" className="show_more" onClick={handleShowMoreReplies}>
Show More Replies ({comment.replies_count - 1})
</a>
</div>
</div>
);
};
If I put useFetch call inside of the handler I hget an error that Hooks can't be called there, but I need to call it only when the button is clicked so I don't know if there is a way to implement that.
I think you have subtle problems in your useFetch hook
1.your useEffect is having dep of ${url} and ${actionType } which you need to define.
2.In order to call this hook by clicking the button, you need to expose the setUrl as follows
const useFetch = ( initialUrl, options, actionType, dataType) => {
const [url, setUrl ] = useState(initialUrl);
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url);
const data = await res.json();
dispatch({ payload: data, type: actionType });
} catch (error) {
console.log(error);
}
};
fetchData();
}, [url, actionType]);
const data = useSelector(state => state[dataType]);
return [ data, setUrl ];
};
export default useFetch;
Then when you are trying to use this hook, you can
const [data, fetchUrl] = useFetch(
`/api/comment_replies?comment_id=${comment.id}`,
null,
"REPLIES_RECEIVED",
"replies"
);
Then every time you have a button you can simply call
fetchUrl(${yourUrl}).
your hook will receive the new URL, which is the dep of your hook and rerender it.
Here is an related article
https://www.robinwieruch.de/react-hooks-fetch-data