React redux dispatching action synchronously - javascript

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

Related

Please report: Excessive number of pending callbacks: 501 when called inside of useEffect

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()
}
}, [])

How to get react component with useReducer to rerender after axios call?

I am trying to learn state management with the useReducer hook so I have built a simple app that calls the pokeAPI. The app should display a random pokemon, and add more pokemons to the screen as the 'capture another' button is pressed.
However, it rerenders the component with the initialized and empty Card object before populating the Card from the axios call. I've tried at least 3 different solutions based on posts from stackoverflow.
In each attempt I have gotten the same result: the app displays an undefined card on, even though the state is updated and not undefined, it just was updated slightly after the rerendering. When clicked again that prior undefined gets properly rendered but there is now a new card displayed as undefined.
I am still getting the hang of react hooks (no pun intended!), async programming, and JS in general.
Here is the app:
https://stackblitz.com/edit/react-ts-mswxjv?file=index.tsx
Here is the code from my first try:
//index.tsx
const getRandomPokemon = (): Card => {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
//omitted for brevity
})
.catch((error) => {
//omitted
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
//omitted
})
.catch((error) => {
//omitted
});
return newCard;
};
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function addCard() {
let newCard: Card = getRandomPokemon();
dispatch({
type: ActionKind.Add,
payload: newCard,
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={addCard}>Catch Another</CatchButton>
</div>
);
};
//cardReducer.tsx
export function cardReducer(state: Card[], action: Action): Card[] {
switch (action.type) {
case ActionKind.Add: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
clonedState = [...clonedState, action.payload];
return clonedState;
}
default: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
return clonedState;
}
}
}
//Deck.tsx
//PokeDeck and PokeCard are styled-components for a ul and li
export const Deck = ({ deck }: DeckProps) => {
useEffect(() => {
console.log(`useEffect called in Deck`);
}, deck);
return (
<PokeDeck>
{deck.map((card) => (
<PokeCard>
<img src={card.image} alt={`image of ${card.name}`} />
<h2>{card.name}</h2>
</PokeCard>
))}
</PokeDeck>
);
};
I also experimented with making the function that calls Axios a promise so I could chain the dispatch call with a .then.
//index.tsx
function pokemonPromise(): Promise<Card> {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
return new Promise((resolve) => {
resolve(newCard);
});
}
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function asyncAdd() {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
console.log(newCard);
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={asyncAdd}>Catch Another</CatchButton>
</div>
);
};
I also tried to have it call it with a side effect using useEffect hook
//App.tsx
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={()=>{setCatchCount(catchCount + 1)}>Catch Another</CatchButton>
</div>
);
};
So there are a couple of things with your code, but the last version is closest to being correct. Generally you want promise calls inside useEffect. If you want it to be called once, use an empty [] dependency array. https://reactjs.org/docs/hooks-effect.html (ctrl+f "once" and read the note, it's not that visible). Anytime the dep array changes, the code will be run.
Note: you'll have to change the calls to the Pokemon service as you're running two async calls without awaiting either of them. You need to make getRandomPokemon async and await both calls, then return the result you want. (Also you're returning newCard but not assigning anything to it in the call). First test this by returning a fake data in a promise like my sample code then integrate the api if you're having issues.
In your promise, it returns a Card which you can use directly in the dispatch (from the response, you don't need the extra step). Your onclick is also incorrectly written with the brackets. Here's some sample code that I've written and seems to work (with placeholder functions):
type Card = { no: number };
function someDataFetch(): Promise<void> {
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
async function pokemonPromise(count: number): Promise<Card> {
await someDataFetch();
console.log("done first fetch");
await someDataFetch();
console.log("done second fetch");
return new Promise((resolve) =>
setTimeout(() => resolve({ no: count }), 1000)
);
}
const initialState = { name: "pikachu" };
const cardReducer = (
state: typeof initialState,
action: { type: string; payload: Card }
) => {
return { ...state, name: `pikachu:${action.payload.no}` };
};
//App.tsx
const App = () => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
pokemonPromise(catchCount)
.then((newCard) => {
dispatch({
type: "ActionKind.Add",
payload: newCard
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
{deck.name}
<button onClick={() => setCatchCount(catchCount + 1)}>
Catch Another
</button>
</div>
);
};

dispatching actions on every render

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

event fire on onclick in react-redux

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.

Correct way to cancel async axios request in a React functional component

What is the correct approach to cancel async requests within a React functional component?
I have a script that requests data from an API on load (or under certain user actions), but if this is in the process of being executed & the user navigates away, it results in the following warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Most of what I have read solves this with the AbortController within the componentDidUnmount method of a class-based component. Whereas, I have a functional component in my React app which uses Axois to make an asynchronous request to an API for data.
The function resides within a useEffect hook in the functional component to ensure that the function is run when the component renders:
useEffect(() => {
loadFields();
}, [loadFields]);
This is the function it calls:
const loadFields = useCallback(async () => {
setIsLoading(true);
try {
await fetchFields(
fieldsDispatch,
user.client.id,
user.token,
user.client.directory
);
setVisibility(settingsDispatch, user.client.id, user.settings);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
}, [
fieldsDispatch,
user.client.id,
user.token,
user.client.directory,
settingsDispatch,
user.settings,
]);
And this is the axios request that is triggered:
async function fetchFields(dispatch, clientId, token, folder) {
try {
const response = await api.get(clientId + "/fields", {
headers: { Authorization: "Bearer " + token },
});
// do something with the response
} catch (e) {
handleRequestError(e, "Failed fetching fields: ");
}
}
Note: the api variable is a reference to an axios.create object.
To Cancel a fetch operation with axios:
Cancel the request with the given source token
Ensure, you don't change component state, after it has been unmounted
Ad 1.)
axios brings its own cancel API:
const source = axios.CancelToken.source();
axios.get('/user/12345', { cancelToken: source.token })
source.cancel(); // invoke to cancel request
You can use it to optimize performance by stopping an async request, that is not needed anymore. With native browser fetch API, AbortController would be used instead.
Ad 2.)
This will stop the warning "Warning: Can't perform a React state update on an unmounted component.". E.g. you cannot call setState on an already unmounted component. Here is an example Hook enforcing and encapsulating mentioned constraint.
Example: useAxiosFetch
We can incorporate both steps in a custom Hook:
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
React.useEffect(() => {
const source = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: source.token })
.then(res => { if (isMounted) onFetched(res); })
.catch(err => {
if (!isMounted) return; // comp already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
source.cancel();
};
}, [url, onFetched, onError, onCanceled]);
}
import React from "react";
import axios from "axios";
export default function App() {
const [mounted, setMounted] = React.useState(true);
return (
<div>
{mounted && <Comp />}
<button onClick={() => setMounted(p => !p)}>
{mounted ? "Unmount" : "Mount"}
</button>
</div>
);
}
const Comp = () => {
const [state, setState] = React.useState("Loading...");
const url = `https://jsonplaceholder.typicode.com/users/1?_delay=3000&timestamp=${new Date().getTime()}`;
const handlers = React.useMemo(
() => ({
onFetched: res => setState(`Fetched user: ${res.data.name}`),
onCanceled: err => setState("Request canceled"),
onError: err => setState("Other error:", err.message)
}),
[]
);
const cancel = useAxiosFetch(url, handlers);
return (
<div>
<p>{state}</p>
{state === "Loading..." && (
<button onClick={cancel}>Cancel request</button>
)}
</div>
);
};
// you can extend this hook with custom config arg for futher axios options
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
const cancelRef = React.useRef();
const cancel = () => cancelRef.current && cancelRef.current.cancel();
React.useEffect(() => {
cancelRef.current = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: cancelRef.current.token })
.then(res => {
if (isMounted) onFetched(res);
})
.catch(err => {
if (!isMounted) return; // comp already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
cancel();
};
}, [url, onFetched, onError, onCanceled]);
return cancel;
}
useEffect has a return option which you can use. It behaves (almost) the same as the componentDidUnmount.
useEffect(() => {
// Your axios call
return () => {
// Your abortController
}
}, []);
You can use lodash.debounce and try steps below
Stap 1:
inside constructor:
this.state{
cancelToken: axios.CancelToken,
cancel: undefined,
}
this.doDebouncedTableScroll = debounce(this.onScroll, 100);
Step 2:
inside function that use axios add:
if (this.state.cancel !== undefined) {
cancel();
}
Step 3:
onScroll = ()=>{
axiosInstance()
.post(`xxxxxxx`)
, {data}, {
cancelToken: new cancelToken(function executor(c) {
this.setState({ cancel: c });
})
})
.then((response) => {
}

Categories