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

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.

Related

How can I stop react from making multiple network request simultaneously

I am making a get request to get data from my rest API, and when I have the data react will keep on making the same request simultaneously multiple times.
this is the code:
export default function Transaction() {
const [transactions, setTransaction] = useState([]);
const [loading, setLoading] = useState(true)
const { id } = useParams();
// this is where I am getting the problem
useEffect(() => {
const fetchTransc = async () => {
const res = await axios.get(`http://localhost:4000/api/get-records/${id}`);
setTransaction(res.data)
setLoading(false)
console.log(res.data)
};
fetchTransc();
},[id,transactions]);
The second argument of the UseEffect hook is the dependency array. It tells react which variables to watch for changes. When a dependency changes or when the component is mounted for the first time, the code inside the hook is executed.
In your case the array indicates that each time “id” or “transations” change, the hook should be executed.
Since setTransation is called in the fetch function, that will trigger the hook again because “transations” is present in hook’s the dependency array.
Each time the transactions state variable is set with a brand a new object fetched from the url, react will trigger the useEffect hook.
If “transations” is removed from the hook’s dependency array, this should work fine. Maybe also adding an IF to check the “id” value could be useful to prevent errors.
useEffect(() => {
const fetchTransc = async () => {
if(id != null) {
const res = await axios.get(`http://localhost:4000/api/get-records/${id}`);
setTransaction(res.data)
setLoading(false)
console.log(res.data)
}
};
fetchTransc();
},[id]);

React - Error handling useState(). How to reset errors?

Introduction
I am handling all the errors in my app. I have noticed there are different types of errors that can be handled in React...
Errors with rendering (undefined props, ...), which can be handled with an Error Boundary Component, with custom Fallback Components.
Errors coming from the backend, which I want to display in a toast with i18n.js for translating the messages.
Is it common to combine these two types of error handling in React apps?
Displaying errors in a toast
I have decided to create my own React Context which renders toasts for notifications, errors, etc. Here is my implementation:
/* eslint-disable react/prop-types */
import React, {
createContext,
useReducer,
useMemo,
} from "react";
import toastReducer, {
initialState,
actionCreators,
} from "./helpers/reducers/toastReducer";
import { TOAST_TYPES, DEFAULT_TOAST_DURATION } from "../../utils/toast";
import Toast from "../../components/Toast";
const ToastContext = createContext(null);
export default ToastContext;
export function ToastProvider({ children }) {
const [toast, dispatch] = useReducer(toastReducer, initialState);
const open = (
type = TOAST_TYPES[0],
message,
duration = DEFAULT_TOAST_DURATION,
action = undefined
) => {
dispatch(actionCreators.open(type, message, duration, action));
};
const close = () => {
dispatch(actionCreators.close());
};
const value = useMemo(() => ({
open,
close,
}), []);
return (
<ToastContext.Provider value={value}>
{children}
<Toast
visible={toast.visible}
type={toast.type}
duration={toast.duration}
action={toast.action}
onDismiss={close}
>
{toast.message}
</Toast>
</ToastContext.Provider>
);
}
I have also implemented my useToast() hook that consumes this context.
Fetching data from server
As I am using hooks, I am saving the errors in a state "error" on each custom hook that fetches data from my DB.
// Based on the library "useQuery"
const useFetchSomeData = ({ fetchOnMount = true } = {}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cursor = useRef(new Date());
const isFetching = useRef(false);
const fetchData = async () => {
if (isFetching.current) return;
isFetching.current = true;
setLoading(true);
try {
const data = await api.fetchData(cursor.current);
} catch(err) {
setError(err);
} finally {
setLoading(false);
isFetching.current = false;
}
}
useEffect(() => {
if (fetchOnMount) {
fetchData();
}
}, []);
return {
data,
loading,
error,
fetchData
}
}
And in my components I just do:
const { data, error, loading } = useFetchData();
const toast = useToast();
const { t } = useI18N();
useEffect(() => {
if (error) {
toast.open("error", t(err.code));
}
}, [error, t]);
Questions
As you can see, in my components, I am just using a useEffect for sending the translated errors to my toast context. But... what if I get the same error response from the server twice? I mean, the error is not reset to null in the custom hook... how can I do that?
Is this implementation robust (in your opinion) and typical in these situations?
Honestly I'm a bit confused, I think I understand what you are trying to do but it doesn't have to be this complicated.
You should wrap your entire app with a ToastContainer or ToastContext (whatever you want to call it) and import your toast whenever needed then call Toat.show("Error: ..."). Checkout the library react-toastify for more details, maybe this library could help you. You wrap your project at the app entry level and never have to worry about it again.
As far as error handling, when creating a function to fetch data you should expect one error at a time and handle it appropriately. I personally think it's best to return the error to the original component that called the fetch function and handle it there. It's easier to keep track of things this way in my opinion, but what you're doing is not wrong.
Error handling takes time and thinking ahead, I'm not gonna deny it, but it makes a big different between a badly constructed app and a well thought out app.
I'm also not completely sure why you are using const isFetching = useRef(false); when you could just use const [isFetching, setIsFecthing] = useState(false), this is what useState is for. Same for useMemo. I think things could be leaner here, as you app grows and becomes more complex it'd easier to maintain.

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

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.

Custom hooks not working properly with useEffect

I wasn't sure on a title for this issue, but I can better explain it here. I have a custom hook that is relying on some information. Part of the information must rely on an async call.
I have three situations happening.
Tried conditionally rendering the custom hook, but react does not like that due to rendering more hooks on a different render.
The custom hook is only mounting once and not passing in the updated information it needs.
I tried passing the dependency to the custom hook and it causes an infinite loop.
Here is a small example of what I'm doing.
Custom Hook:
export function useProducts(options){
const [products, setProducts] = useContext(MyContext)
useEffect(() => {
// only gets called once with `options.asyncValue` === null
// needs to be recalled once the new value is passed in
const loadProducts = async () => {
const data = await asyncProductReq(options)
setProducts(data)
}
loadProducts()
}, []) // if I pass options here it causes the infinite loop
return [products, setProducts]
}
Inside function calling:
export function(props){
const [asyncValue, setValue] = useState(null)
useEffect(() => {
const loadValue = async () => {
const data = await asyncFunc()
setValue(data)
}
loadValue()
}, []}
const options = {...staticValues, asyncValue}
const [products] = useProducts(options)
return (
<h2>Hello</h2>
)
}
I know that I need to pass the options to be a dependency, but I can't figure out why it's causing an infinite reload if the object isn't changing once the async call has been made inside the func.
You were correct in adding options in the dependencies list for your custom hook.
The reason it is infinitely looping is because options IS constantly changing.
The problem is you need to take it one step further in the implementation and make use of the useMemo hook so options only changes when the async value changes, instead of the whole component changing.
So do this:
const options = React.useMemo(() => ({...staticValues, asyncValue}), [asyncValue])

How can I re-fetch an API using react hooks

devs,
I have decided to finally learn react hooks with what I thought would be a simple project. I can't quite figure out how I re-fetch an API using react hooks. Here is the code I have so far.
import React, { useState, useEffect } from "react"
import useFetch from "./utils/getKanya"
const kanye = "https://api.kanye.rest"
const Index = () => {
let [kanyaQuote, setKanyeQuote] = useState(null)
let data = useFetch(kanye)
const getMore = () => {
setKanyeQuote(useFetch(kanye))
}
return (
<>
<h1>Welcome to Next.js!</h1>
<p>Here is a random Kanye West quote:</p>
{!data ? <div>Loading...</div> : <p>{!kanyaQuote ? data : kanyaQuote}</p>}
<button onClick={getMore}>Get new quote</button>
</>
)
}
export default Index
I get the kanyeQuote state value to null
I fetch the initial data
I either show "Loading..." or the initial quote
I am trying to set up a button to re-fetch the API and store the data in kanyeQuote via getKanyeQuote (setState)
This is the error I get Error: Invalid hook call...
I would greatly appreciate any guidance you can provide on this.
The issue here is, that you can only use hooks directly inside the root of your component.
It's the number 1 'rule of hooks'. You can read more about that here
const getMore = () => {
setKanyeQuote(useFetch(kanye) /* This cannot work! */)
}
There are a few ways you could work around that. Without knowing the internal logic in your useFetch-hook I can only assume you are able to change it.
Change hook to handle its state internally
One way to work around that would be to change the logic of your custom useFetch hook to provide some form of function that fetches the data and updates the state internally. It could then look something like this:
const { data, doFetch } = useFetch(kanye);
useEffect(() => {
doFetch(); // initialFetch
}, []);
const getMore = () => {
doFetch();
};
// ...
You would then need to change the internal logic of your useFetch-hook to use useState internally and expose the getter of it. It would look something like this:
export const useFetch = (url) => {
const [data, setData] = useState(null);
const doFetch = () => {
// Do your fetch-Logic
setData(result);
};
return { data, doFetch };
};
Change hook not to handle any state at all.
If you only want to manage the state of the loaded data in the parent component, you could just provide the wrapped fetch function through the hook; Something like that:
const doFetch = useFetch(kanye);
const [data, setData] = useState(null);
useEffect(() => {
setData(doFetch()); // initialFetch
}, []);
const getMore = () => {
setData(doFetch())
};
// ...
You would then need to change the internal logic of your useFetch-hook to not have any internal state and just expose the wrapped fetch. It would look something like this:
export const useFetch = (url) => {
const doFetch = () => {
// Do your fetch-Logic
return result;
};
return doFetch;
};

Resources