i face this issue.
when i have a Axios call, which promise to dispatch a action to update the redux and execute the callback.
but when callback is executed, the redux state seem to be stale.
i got a sandbox code for demo here
if you click on the getNewDate Button, the console will show the difference in the redux state.
the state will be correct when redux cause a re-render.
How do i get the correct redux state during callback?
The response will always be stale, that's how React hooks work. They apply a closure over all the variables in each individual render when they are created. If you absolutely need the value to not be stale in a callback function (or effect), set up a ref for it.
const { response, getNewDate } = useResponse();
const responseRef = useRef(response);
useLayoutEffect(() => {
responseRef.current = response;
}, [response]);
const callbackSuccessful = (data: IResponse) => {
console.log("response is not Stale: " + responseRef.current.newDate);
console.log("should be: " + data.newDate);
};
Once you set up a ref, you'll clearly see that the response is in-fact changing and the responseRef.current shows the same value as data.newDate.
You have to useLayoutEffect here because the order in which the effect runs is wrong for the callback. Since useEffect runs after the component re-renders while useLayoutEffect runs while the component re-renders.
Another way you could see that the useSelector is working fine and updating and that your MyPages.tsx is seeing that update is useEffect to log the change whenever it changes.
useEffect(() => {
console.log(response.newDate)
}, [response]);
If you want access to the latest redux store state in a callback without any timing issues at all, useStore is helpful, and it doesn't cause re-rendering at all.
const store = useStore();
const callbackSuccessful = (data: IResponse) => {
console.log("should be: " + data.newDate);
console.log("Redux store: " + store.getState().apiResponse.newDate);
};
https://codesandbox.io/s/react-typescript-demo-pek65?file=/src/pages/MyPages.tsx
The use previous answer of Zachary Haber's useLayoutEffect is the correct answer.
But here are two subpar solutions that I can share, both with their own issues.
Solution 1
Use a key on the button to inform React that it should renew the scope of the button object (i.e. re-mount the component) because some of it's dependencies have updated.
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const MyPages = () => {
const { response, getNewDate } = useResponse();
const callbackSuccessful = (data: IResponse) => {
console.log("response is: " + response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
const buttonKey = response.newDate
return <div>
hello world {response.newDate}
<br/>
<button key={buttonKey} type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
A slight problem: This will display the previous result, i.e. response.newDate will contain the value it had when the key was changed.
Solution 2
Use a global variable e.g. state
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const state = {
response: undefined
}
const MyPages = () => {
const { response, getNewDate } = useResponse();
state.response = response;
const callbackSuccessful = (data: IResponse) => {
console.log("state response is: " + state.response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
return <div>
hello world {response.newDate}
<br/>
<button type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
In the call back the state.response.newDate is equal to data.newDate.
A slight problem: It's not a reusable component anymore. This solution will only work if you ever have one MyPages object in your app. All instances will point to the same static global variable and this will introduce a contention of whoever writes last wins.
You should not do:
<MyPages />
<MyPages />
Another issue I have with this solution: React's optimization; I don't know how this will affect it.
I hope this helps.
Related
I've been following the official Redux tutorial to create and dispatch asynchronous thunks (created with createAsyncThunk) to load some user state.
I have two components, say A and B, and they both need access to the same userState information. I'm not sure which will complete rendering first, so I've added the ability to dispatch the information they need to both:
// both in component A and B ...
const status = useSelector(selectUserStateStatus);
useEffect(()=>{
if (status === "idle") {
dispatch(fetchUserState());
}
}, [status] );
//...
However, both are consistently dispatching fetchUserState() so I wind up with unnecessary duplicate requests.
I would think that (in the scenario that the useEffect triggers in A first):
A completes render, useEffect is called when status==="idle"
fetchUserState is dispatched, setting status = "loading"
A re-render of B is triggered, with status === "loading"
B doesn't send a new request
However, this is not what happens. A and B both dispatch fetchUserState, resulting in duplicate dispatches.
If I use store.getState() however, expected behavior results with only a single dispatch. IE:
useEffect(()=>{
if (store.getState().status === "idle") {
dispatch(fetchUserState());
}
}, [store.getState().status] );
This feels like an anti-pattern though. I'm confused by this behavior, any suggestions on how to remedy this or archetype it more effectively?
use lodash/throttle on the API layer for cancelling first request
use common handler for all nesting components
function UserStateProvider() {
const isInitRef = useRef(false)
const status = useSelector(selectUserStateStatus);
const fetchUser = () => {
if (isInitRef.current === false) {
dispatch(fetchUserState())
isInitRef.current = true
}
}
return <ContextProvider value={{
status: status, // if you need status
fetchUser: fetchUser,
}}>
{children}
</ContextProvider>
}
const A = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
<UserStateProvider>
<A />
<B />
<A />
</UserStateProvider>
// sugar, if all what you want it is trigger loading user
const useLoadUser = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
const A = () => {
useLoadUser()
}
Using useEffect I am calling some data from different URLs through axios and trying to change the URL on every render, I am using a state to change URL and whenever the state changes, the URL will also change and run the useeffect again with different URL. But to change the state in the render I am using a button, My question is there any ways to make the state change and run the API call without the button click.
import React,{useState,useEffect} from "react";
import axios from "axios";
export default function TrackLoader(accessToken){
const [number, setNumber] = useState(0);
const [arrayOfPlayLists, setArrayOfPlayLists] = useState([]);
const playListIds = ["3d93aG1WkqLxS2q9GkUgXO", "37i9dQZF1DWX3xqQKu0Sgn", "37i9dQZF1DWU13kKnk03AP", "37i9dQZF1DXdPec7aLTmlC", "37i9dQZF1DX889U0CL85jj", "37i9dQZF1DXbVipT9CLvYD"]
useEffect(() =>{
async function getData(){
const response = await axios.get(`https://api.spotify.com/v1/playlists/${playListIds[number]}`,{
headers:{
Authorization:"Bearer " + accessToken.accessToken,
},
})
let selectedPlayList = response.data
setplayList(selectedPlayList)
arrayOfPlayLists.push(selectedPlayList)
}
getData();
},[number]);
return(
<div>
{number ===5? null: <button onClick={()=> setNumber(number+1)}></button>}
</div>
)
}
And also additionally I am trying to put the data I got from API in an array. Sorry for my English.
You can add another useEffect with setTimeout
useEffect(() => {
if(number <= 5) {
setTimeout(() => {
setNumber(number + 1);
}, 1000);
}
}, [number]);
I am trying to learn to work with custom Hooks in React-native. I am using AWS Amplify as my backend, and it has a method to get the authenticated user's information, namely the Auth.currentUserInfo method. However, what it returns is an object and I want to make a custom Hook to both returns the part of the object that I need, and also abstract away this part of my code from the visualization part. I have a component called App, and a custom Hook called useUserId. The code for them is as follows:
The useUserId Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
const userId = getUserInfo();
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, [userId]);
return id;
};
export default useUserId;
The App component:
import React from "react";
import useUserId from "../custom-hooks/UseUserId";
const App = () => {
const authUserId = useUserId();
console.log(authUserId);
However, when I try to run the App component, I get the same Id written on the screen twice, meaning that the App component is executed again.
The problem with this is that I am using this custom Hook in another custom Hook, let's call it useFetchData that fetches some data based on the userId, then each time this is executed that is also re-executed, which causes some problems.
I am kind of new to React, would you please guide me on what I am doing wrong here, and what is the solution to this problem. Thank you.
The issue is likely due to the fact that you've declared userId in the hook body. When useUserId is called in the App component it declares userId and updates state. This triggers a rerender and userId is declared again, and updates the state again, this time with the same value. The useState hook being updated to the same value a second time quits the loop.
Bailing out of a state update
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Either move const userId = getUserInfo(); out of the useUserId hook
const userId = getUserInfo();
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, []);
return id;
};
or more it into the useEffect callback body.
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
and in both cases remove userId as a dependency of the useEffect hook.
Replace userId.then with to getUserId().then. It doesn't make sense to have the result of getUserId in the body of a component, since it's a promise and that code will be run every time the component renders.
I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.
It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
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.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.