Component state doesn't change after action dispatch - reactjs

On startup I use useEffect to fetch some data from local storage. If the fetch was successful I want to send an action and change the state of the component. After dispatching the action, I see that the reducer received an action and returned the new state, but I don't see any change in the component when I try to log the new state. What can be the reason for this behaviour.
Here's the action.ts:
import {TOKEN_VALIDITY} from './actionTypes'
export const setTokenValidity = (isTokenValid: boolean) => ({
type: TOKEN_VALIDITY,
isTokenValid
})
Here's the reducer.ts:
const auth = (state = false, action: any) => {
// after dispatch I see this log in the console value is true
console.log('auth reducer action type is ' + action.type + ' value is ' + action.isTokenValid)
switch(action.type) {
case TOKEN_VALIDITY:
return action.isTokenValid
default:
return state
}
}
export default auth
In the component I want to update the state of isTokenValid, but I always get undefined for the value.
This is the component code
const Stack = createStackNavigator();
let userToken = null;
const App = (props:any) => {
useEffect(() => {
const bootstrapAsync = async () => {
try {
userToken = await retrieveData('Token',null);
SplashScreen.hide();
if(userToken != null) {
props.setTokenValidity(true)
}
// this logs undefined for props.isTokenValid -- why???
console.log("after token isValid: " + props.isTokenValid)
console.log('token ' + userToken);
} catch (e) {
// Restoring token failed
}
};
bootstrapAsync();
}, []);
return (
<NavigationContainer>
<Stack.Navigator >
{!props.isTokenValid ? (
<>
<Stack.Screen name="Login" component={Login} options={{ headerShown:false }}/>
<Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
</>
) : (
<Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
)}
</Stack.Navigator >
</NavigationContainer>
);
};
const mapDispatchToProps = (dispatch:any) => ({
setTokenValidity: (isTokenValid:boolean) => dispatch(setTokenValidity(isTokenValid))
})
const mapStateToProps = (state:any) => ({
isTokenValid: state.isTokenValid
})
export default connect(mapStateToProps, mapDispatchToProps)(App);

In the first rendering, i.e. when the useEffect is fired and you call the method using props.setTokenValidity to set the token validity, the token gets set. However, the console also gets printed on the same rendering.
When the state gets updated and you get an updated one using the props.isTokenValid, this is the 2nd re-rendering(not the same rendering when useEffect was called) and the useEffect doesn't fire, therefore we don't see the console being printed with the new value.
If you for some reason want to log when isTokenValid is set, use another useEffect
useEffect(() => {
console.log("after token isValid: " + props.isTokenValid)
},[props.isTokenValid]);

Reducers return a state object so you might wanna try doing this:
switch(action.type) {
case TOKEN_VALIDITY:
return { ...state, isTokenValid: action.isTokenValid }
default:
return state
}

The way I solved it in my project, is by creating multiple useEffects.
In order to solve your problem you need to do 2 things:
You need to run bootstrapAsync()
You need to check after bootstrapAsync finishes for the data that was set into userToken
You have already did no.1 successfully, And your current problem is that your component does not updates when it receives new data, aka when userToken updates.
The solution:
Write another useEffect function which will be rendered 2 times: one time on component load(which we will ignore because the fetch isn't done yet) and another time when userToken value updates.
In order to avoid running our new useEffect on component load, we need to create a new state, which we will call allowNavigation.
allowNavigation prop will be set to true only after the fetch is complete.
Then, only when allowNavigation is set to true, we can check userToken and handle it properly.
The following code will help you:
const App = (props:any) => {
const [allowNavigate, setAllowNavigate] = useState(false);
useEffect(() => {
const bootstrapAsync = async () => {
try {
userToken = await retrieveData('Token',null);
SplashScreen.hide();
setAllowNavigate(true);
...
};
bootstrapAsync();
}, []);
useEffect(() => {
if (allowNavigate)
// this will now log the correct values
console.log("after token isValid: " + props.isTokenValid)
console.log('token ' + userToken);
}, [allowNavigate]);
...

Related

Not able to update state using localStorage

I am trying log back in using the stored credentials but it doesn't work and I have tried everything . The dispatch function works fine with the form but it doesn't works with the localStorage .
App.tsx :
useEffect(() => {
const userDetails=localStorage.getItem('user')
if (userDetails) {
const user= JSON.parse(userDetails);
login(user); // dispatch function
}
});
If you are sure, value in the localStorage and useEffect should be called just once — on the component mount stage; I recommend sending the value through the props. With this approach, it will be much easier to guess what is going on.
const ParentComponent = () => (
<Component
user={
JSON.parse(localStorage.getItem('user'))
}
/>
)
const Component = ({
user
}) => {
useEffect(() => {
login(user); // dispatch function
}, [user]);
return <></>
};

React Native data in context is undefined on the first render

I use AppContext, when I fetch data from server I want it to save in context but on the first render it doesn't save. If I make something to rerender state data appears in context.
Here is my code:
useEffect(() => {
fetch('https://beautiful-places.ru/api/places')
.then((response) => response.json())
.then((json) => myContext.updatePlaces(json))
.then(() => console.log('jsonData', myContext.getPlaces()))
.catch((error) => console.error(error));
}, []);
My getPlaces and updatePlaces methods:
const [allPlaces, setAllPlaces] = useState();
const getPlaces = () => {
return allPlaces;
};
const updatePlaces = (json) => {
setAllPlaces(json);
};
const placesSettings = {
getPlaces,
updatePlaces,
};
Here is how I use AppContext:
<AppContext.Provider value={placesSettings}>
<ThemeProvider>
<LoadAssets {...{ assets }}>
<SafeAreaProvider>
<AppStack.Navigator headerMode="none">
<AppStack.Screen
name="Authentication"
component={AuthenticationNavigator}
/>
<AppStack.Screen name="Home" component={HomeNavigator} />
</AppStack.Navigator>
</SafeAreaProvider>
</LoadAssets>
</ThemeProvider>
</AppContext.Provider>;
Could you explain please why my console.log('jsonData', ...) returns undefined?
I don't understand because on previous .then I saved it.
Edit to note that the code below is not copy-paste ready. It is an example of how to attack the problem – you will need to implement it properly in your project.
The 'problem' is that hooks are asynchronous – in this specific case, your useEffect further uses an asynchronous fetch too.
This means that the data that is returned by the fetch will only be available after the component has rendered, and because you're not updating state/context using a hook, the context won't update.
The way to do this requires a few changes.
In your context implementation, you should have a setter method that sets a state variable, and your getter should be that state variable.
placesContext.js
import React, { createContext, useState } from "react";
export const placesContext = createContext({
setPlaces: () => {},
places: [],
});
const { Provider } = placesContext;
export const PlacesProvider = ({ children }) => {
const [currentPlaces, setCurrentPlaces] = useState(unit);
const setPlaces = (places) => {
setCurrentPlaces(places);
};
return (
<Provider value={{ places: currentPlaces, setPlaces }}>{children}</Provider>
);
};
Wrap your App with the created Provider
App.js
import { PlacesProvider } from "../path/to/placesContext.js";
const App = () => {
// ...
return (
<PlacesProvider>
// Other providers, and your app Navigator
</PlacesProvider>
);
}
Then, you should use those variables directly from context.
MyComponent.js
import { placesContext } from "../path/to/placesContext.js";
export const MyComponent = () => {
const { currentPlaces, setPlaces } = useContext(placesContext);
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
async function fetchPlacesData() {
const placesData = await fetch('https://beautiful-places.ru/api/places');
if (placesData) {
setPlaces(placesData);
} else {
// error
}
setHasLoaded(true);
}
!hasLoaded && fetchPlacesData();
}, [hasLoaded]);
return (
<div>{JSON.stringify(currentPlaces)}</div>
)
};

Can't perform a React state update on an unmounted component useEffect error

I have this code where I am fetching data and passing it to a component. This child component then renders the data I have made it so that when the user pulls down from the top, the component will refresh but whenever I refresh for the first time only, I get the error
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.
Here is the code:
function FolloweringScreens({
data,
screen,
username,
navigation,
noMoreData,
setLoadingMore,
lastVisible,
setNoMoreData,
setLastVisible,
setfollowingData,
loadingMore,
setLoading,
currentuser,
}) {
const [stateData, setStateData] = useState();
const [refresh, setRefresh] = useState(false);
useEffect(() => {
const setState = () => setStateData(data);
return setState();
}, [data, refresh]);
// Refresh
const handleFetch = () => {
setRefresh(true);
const cleanup = fetchData(
username,
setfollowingData,
setLoading,
setLastVisible,
setNoMoreData,
setRefresh,
screen,
currentuser,
);
return cleanup;
};
return (
<>
<FlatList
refreshing={refresh}
onRefresh={handleFetch}
data={stateData}
keyExtractor={(i, index) => index.toString()}
renderItem={({item, index}) => {
return (
<>
{ Using my data here }
</>
);
}}
/>
</>
);
}
export default FolloweringScreens;
Here is the fetchData function:
export const fetchData = (
username,
setfollowingData,
setLoading,
setLastVisible,
setNoMoreData,
setRefresh,
screen,
currentuser,
) => {
const dataaRef = firestore().collection('usernames');
setNoMoreData && setNoMoreData(false);
// If in
dataaRef // Go to whichever users clicked on data
.doc(username.toLowerCase())
.collection(screen) // Go to followers/following
.orderBy('followedAt')
.limit(6)
.get()
.then((snapshot) => {
setLoading(true);
snapshot.empty
? null
: setLastVisible(
snapshot.docs[snapshot.docs.length - 1].data().followedAt,
);
let promises = [];
// 1b. For each document, return that document data
snapshot.forEach((doc) => {
const data = doc.data();
promises.push(
data.path.get().then((res) => {
const userData = res.data();
// Go to logged in users directory to see
// if they are following these users in this
// users following/followers page so we can
// differentiate whether to display follow/unfollow button
return dataaRef
.doc(
currentuser === undefined
? username.toLowerCase()
: currentuser.toLowerCase(),
)
.collection('Following')
.doc(doc.id)
.get()
.then((searchedDocs) => {
return {
profileName: doc.id ? doc.id : null,
displayName: userData.displayName
? userData.displayName
: null,
followerCount:
userData.followers !== undefined ? userData.followers : 0,
followingCount:
userData.following !== undefined ? userData.following : 0,
imageUrl: userData.imageUrl ? userData.imageUrl : null,
isFollowed: searchedDocs.exists ? true : false,
};
});
}),
);
});
// 1c. set All document data to followingData
Promise.all(promises).then((res) => {
setfollowingData(res);
// console.log('res', res);
});
setLoading(false);
setRefresh && setRefresh(false);
});
};
You can't do that actually. Since the function that returns from useEffect act as a clean up function. You usually clean up your back like removing event listeners and such things when a component dispose.
useEffect(() => {
document.addEventListenter('click', () => {});
function cleanup() {
document.removeEventListener('click', () => {});
};
return cleanup;
})
This is how useEffect works. So in your case it is complaining about an state update when component is getting unmounted which is illegal in react world.
If you are trying to call it just call it.
useEffect(() => {
const setState = () => setStateData(data);
setState();
}, [data, refresh]);
Or a better way is to define your function outside of useEffect, and call it inside of it.
const setState = () => setStateData(data);
useEffect(() => {
if (!data) return;
setState();
}, [data, refresh]);

React redux shows previous state old data before fetching new data

I use React-Redux and functional components with hooks.
Whenever I send a request for fetching data from the server I set isLoading field in global state to true and then after request is done back to false.
In the rendering component I either show the fetched data or a loading screen if isLoading is set to true. Now the problem is whenever I change page and then reopen the sreen with data, before showing loading screen react renders previous state data from the previous fetch for a brief moment.
Hope someone can explain to me how to avoid that behavior and the reasons why it acts that way.
Here's an example code of what I'm talking about. If I change routes and go back I can see the old data:
//App.js
function App() {
return (
<BrowserRouter>
<Header/>
<Route exact path='/' component={ProductList}/>
<Route exact path='/NotHome' component={NotHome}/>
</BrowserRouter>
);
}
//ProductList.js
const ProductList = (props) => {
const [Skip, setSkip] = useState(0);
const [Limit] = useState(5);
useEffect(() => {
props.requestProductList(Skip, Limit);
}, []);
return (
props.isLoading
? <h1>LOADING</h1>
: <div>
{props.productList && props.productList.map(product => (
<div key={product._id}>
<p>{product.productName}</p>
<img src={`http://localhost:5000${product.productMainPicture}`} style={{width: 200, height: 200}}/>
</div>
))}
</div>
)
};
const mapStateToProps = (state) => ({
productList: state.product.productList,
isLoading: state.product.isLoading,
});
const actionCreators = {
requestProductList
};
export default connect(mapStateToProps, actionCreators)(ProductList);
//ProductReducer.js
const PRODUCT_SET_PRODUCT_LIST = 'PRODUCT_SET_PRODUCT_LIST';
const SET_LOADING = 'SET_LOADING';
const initialState = {
productList: [],
isLoading: false
}
export const ProductReducer = (state = initialState, action) => {
switch (action.type) {
case PRODUCT_SET_PRODUCT_LIST:
return {
...state,
productList: [...action.productList],
productsCount: action.productsCount
}
case SET_LOADING:
return {
...state,
isLoading: action.isLoading
}
default:
return state;
}
};
const setProductList = (productList, productsCount) => ({type: PRODUCT_SET_PRODUCT_LIST, productList, productsCount});
const setLoading = (isLoading) => ({type: SET_LOADING, isLoading});
export const requestProductList = (skip, limit) => async (dispatch) => {
dispatch(setLoading(true));
try {
const res = await productApi.requestProductList(skip, limit);
dispatch(setProductList(res.data.products));
dispatch(setLoading(false));
} catch (e) {
console.log(e);
dispatch(setLoading(false));
}
};
//api.js
export const productApi = {
async requestProductList(skip, limit) {
return await Axios.post(`http://localhost:5000/api/product/get_product_list`, {skip, limit});
}
}
How about clearing the data when leaving the page and then when revisiting, everything should work as expected.
Let me explain,
Lets say you set the Redux state with ObjectA = {...}, then when you leave that page,
objectA still exists with values, so it immediately displays those values. While the network request is asynchronous and takes time to complete the promise and update the objectA.
To solve this, you can create a clearAction, which clears objectA when leaving the page.
useEffect(() =>
{
props.requestProductList(Skip, Limit);
return () =>
{
props.clearData()
}
}, []);
/* Redux reducers*/
case CLEAR_DATA:
return {...state, objectA: null}

problem with using apollo client mutate outside render

I want to have a custom component named AuthRoute not only to check if certain data is available in redux but also verify token with graphql
here is what I've done :
const AuthRoute = ({ component: Component, authUser, ...rest }) => {
const you_must_go_back_to_login = <Route
{...rest}
render={props =>
(<Redirect to={{
pathname: '/user/login',
state: { from: props.location }
}}
/>)
}
/>
const continue_journey = <Route
{...rest}
render={props =>
(<Component {...props} />)
}
/>
const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
useEffect(() => {
client.mutate({
mutation: VERIFY_TOKEN_MUTATION,
variables: { token }
}).then(result => {
// setDest(continue_journey)
setCheckThat(true)
})
return () => {
console.log()
};
}, [])
// if(authUser) {
if (checkThat) {
return continue_journey
} else {
return you_must_go_back_to_login;
}
}
here is the steps I need them to be done:
run mutate
setDest to one of you_must_go_to_login or continue_journey
setCheckThat or something like that to true or false based on token verified or not
return dest as a result
now I've got this error :
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.
UPDATE
I changed my code to this but I still get the same error
const AuthRoute = ({ component: Component, authUser, ...rest }) => {
const you_must_go_back_to_login = (<Route
{...rest}
render={props =>
(<Redirect to={{
pathname: '/user/login',
state: { from: props.location }
}}
/>)
}
/>)
const continue_journey = (<Route
{...rest}
render={props =>
(<Component {...props} />)
}
/>)
// const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
useEffect(() => {
let isSubscribed = true
if (isSubscribed) {
getToken();
}
return () => isSubscribed = false
}, []);
const getToken = async (sig) => {
const data = await mutate(VERIFY_TOKEN_MUTATION, { token })
console.log(data)
setCheckThat(true);
console.log(checkThat)
return;
};
return checkThat ? continue_journey : you_must_go_back_to_login;
}
Error is caused by rendering redirect earlier than getting data.
Normlly ... instead useEffect you can use simple useQuery and use condition loading or !data for rendering some <Loading/> befor decision to redirect or give access (rendering redirect or guarded content).
Mutation is generally used for changing remote data. By using query you can pass variable and return answer, too.
I'm using django graphql jwt and the problem is that verifytoken for that is a mutation
In general/usually token is passed by header for requests and API returns response or errors (for missing/expired token). Usually you have an option to query current user for being logged verification.
... in this case we want to use mutation
... instead client.mutate we can use hooks - useMutation or better API example ( apollo docs :] ) - to get access to loading state before making decision
const [verifyToken, { data, loading, error }] = useMutation( VERIFY_TOKEN_MUTATION, variables: { token } );
Problems?
- mutations are not called at start - loading won't be true on the beggining;
- returned data.
For firing mutation at start we can use useEffect, for returned data we can use onCompleted handler but we can simply use data - should be undefined/nulled at start, usable for conditions.
Something like this should work (we don't need await etc. - data, loading, error will be updated automatically):
useEffect(() => {
verifyToken(); // ariables passed earlier, can be passed here
}, []);
if( !data ) return <Loading />
return data.someSuccessVerifiedField ? continue_journey : you_must_go_back_to_login;
The warning is pretty self-explanatory -
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.
Specially this part -
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
You are using useEffect in a wrong way. You should not put your mutation inside useEffect. What you can do is - take the mutation outside of the useEffect.
const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
client.mutate({
mutation: VERIFY_TOKEN_MUTATION,
variables: { token }
}).then(result => {
// setDest(continue_journey)
setCheckThat(true)
})
return () => {
console.log()
};
useEffect(() => {
// if(authUser) {
if (checkThat) {
return <continue_journey/>
} else {
return <you_must_go_back_to_login/>;
}
}, [checkThat])
}
So now mutation will run, then set checkThat variable. And then useEffect will be triggered which will return based on the value of checkThat

Resources