Custom React-native Hook runs the component that calls it twice. I don't understand why - reactjs

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.

Related

How to use custom hooks if data being passed to the hook needs to be modified beforehand?

I am developing a web application using react. In my project there were multiple instances where I had to send a GET request to the server and receive data and loading status. So I built a custom hook for the same
Here is the custom hook:
export const useGetApi = (link) => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
const handleFetch = async () => {
const cancelToken = axios.CancelToken.source();
try {
const response = await axios.get(link, { cancelToken: cancelToken.token });
setLoading(false);
setData(response.data.data);
} catch (err) {
setLoading(true);
//handle error
}
};
useEffect(() => {
handleFetch();
}, [JSON.stringify(data)]);
return [data, loading];
};
For most cases, it works I only have to pass the link as an argument to the hook. But in some instances, I have the requirement where I will have to first modify the link and then pass the modified link to the hook as a parameter.
I tried to achieve this using:
export const App=()=>{
const link='/helloworld'
const modifiedLink= getModifiedLink(link);
useEffect(()=>{
const [data,loading]= useGetApi(modifiedLink);
},[])
}
I get errors as:
React Hook "useGetApi" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
Do I need to make a separate hook for the cases where the link is modified first and then passed? Because this will lead to a lot of repeatable code. Is there a way to use my existing hook and still fulfill my requirement. Please guide me.
Like React hooks, Custom hooks cannot be called conditionally, inside another hook or in a loop. Custom or not hooks need to be called the top level of the component.
There are several mistakes in your code:
your custom hook is only running once and if you want to refech it like using useEffect (I can see you tried to do this with another useEffect) you can pass the keys to your custom hooks useEffect keys. That way you can refech the query whenever you want.
export const useGetApi = (link) => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
const handleFetch = async () => {
const cancelToken = axios.CancelToken.source();
try {
const response = await axios.get(link, { cancelToken: cancelToken.token });
setLoading(false);
setData(response.data.data);
} catch (err) {
setLoading(true);
//handle error
}
};
useEffect(() => {
handleFetch();
}, [link]); // this way your query runs whenever your link changes
return [data, loading];
};
Using your custom hook inside another hook:
export const App=()=>{
const link='/helloworld'
const modifiedLink= getModifiedLink(link);
const [data,loading]= useGetApi(modifiedLink); // this is the true way
}
You can put the link in a useState hook and send it to your useGetApi so whenever the link changes your query will re-fetch.
I suggest you to use react-query, it basically does what you trying to do here but more stable and easy-to-use approach.

React native screens not re-rendering when custom hook state got changed

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.

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

React Hook - useCustomHook to set outside useState and useRef

I have the main component along with local state using useState and useRef, I also another custom hook, inside the custom hook I would like to reset my main component's state and ref, am I doing correctly by below?
// custom hook
const useLoadData = ({startLoad, setStartLoad, setLoadCompleted, setUserNameRef}) => {
useEffect(() => {
const fetchUser = async() => { await fetchFromApi(...); return userName;};
if (startLoad) {
const newUserName = fetchUser();
setStartLoad(false);
setLoadCompleted(true);
setUserNameRef(newUserName);
}
}, [startLoad]);
}
// main component
const myMainComp = () {
const [startLoad, setStartLoad] = useState(false);
const [loadCompleted, setLoadCompleted] = useState(false);
const userNameRef = useRef("");
const setUserNameRef = (username) => { this.userNameRef.current = username; }
useLoadData(startLoad, setStartLoad, setLoadCompleted, setUserNameRef);
refreshPage = (userId) => {
setStartLoad(true);
}
}
Am I using the custom hook correctly like by passing all external state value and setState method in? Also I found even I don't use useEffect in my customHook, it also works as expected, so do I need to use useEffect in my custom hook? Any review and suggestion is welcome!
First, I think isn't a good approach you use component methods inside custom hook (like "set" methods provided by useState). You are binding the hook with the main component's internal logic. If the purpose of custom hook is fetch data from API, it need to provide to main component the vars that can be able the main component to manipulate its state by itself (like return isFetching, error, data, etc. and don't call any main component set method inside hook).

How do I correctly implement setState() within useEffect()?

If this useEffect() runs once (using [] as the second parameter), setTicket(response.data) does not update the value of ticket with data. If I run useEffect() with [ticket] as the parameter, it updates the value of ticket with data, but useEffect becomes an infinite loop.
I need it to run once and update the ticket data. I don't think I understand useEffect() and its second parameter.
What do I do to get the expected result?
import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
const EditTicket = (props) => {
const [ticket, setTicket] = useState("");
useEffect(() => {
axios
.get("http://localhost:4000/tickets/" + props.match.params.id)
.then((response) => {
setTicket(response.data);
console.log({ ticket });
})
.catch(function (error) {
console.log(error);
});
}, []);
return <div>edit</div>;
};
export default EditTicket;
ticket is a local const. It will never change, and that's not what setTicket is trying to do. The purpose of setTicket is to tell the component to rerender. On that next render, a new local variable will be created, with the new value.
Your code is already written the way it should be written, except that your log statement is not providing you with any useful information. If you want to see that it rerenders with the new value you could move the log statement to the body of the component.
const EditTicket = (props) => {
const [ticket, setTicket] = useState("");
console.log('rendering', ticket);
useEffect(() => {
// same as before

Resources