I'm using axios-hooks in my react project. I have a problem that whenever I re-render the component, the backend is called and at the beginning, the same endpoint is called twice.
I'm using it in a following way:
import useMyHook from '../../hooks/useMyHook ';
export default function MyComponent() {
const { getData } = useMyHook (category);
...
<Button onClick={getData}...
}
**getData is called to refresh the data (so it's normal that the backend is called again here)
export default function useMyHook(category) {
const { language, contextData, dispatch } = useAppContext();
const config = {... url, headers, params ...};
const opts = { manual: false };
const [{ data: myData, loading, error }, reFetch] = useAxios(config, opts);
useEffect(() => {
if (_.isEmpty(contextData) && !_.isEmpty(myData)) {
dispatch({ type: DATA_LOADED, payload: myData});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [myData]);
const getData = () => {
dispatch({ type: DATA_RESET});
reFetch();
};
return { loading, error, getData};
}
Is there something wrong with my implementation?
PS. I've seen that useAxios has
useAxios(){...}, [stringifiedConfig]) and stringifiedConfig=JSON.stringify(config)
and in my understanding, it shouldn't re-call the backend if the config doesn't change.
Basically, the problem is because of state dispatching.
Whenever we change the state, the component that is calling axios-hooks is unmounted and mounted again so we do a second call.
The workaround is to check if the value is not undefined inside useEffect of the component and then call the axios-hook and disable the automatic call {manual: true} in useAxios.
Related
Thanks everyone, especially Mr.Drew Reese. If you are newbie as me, see his answer.
I don't know why but when I console log state data if I use useEffect, it always rerender although state generalInfo not change :/ so someone can help me to fix it and explain my wrong?
I want the result which is the data will be updated when generalInfo changes.
Thanks so much!
This is my useEffect
======================== Problem in here:
const {onGetGeneralInfo, generalInfo} = props;
const [data, setData] = useState(generalInfo);
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [generalInfo]);
======================== fix:
useEffect(() => {
onGetGeneralInfo();
}, []);
useEffect(() => {
setData(generalInfo);
}, [generalInfo, setData]);
this is mapStateToProps
const mapStateToProps = state => {
const {general} = state;
return {
generalInfo: general.generalInfo,
};
};
this is mapDispatchToProps
const mapDispatchToProps = dispatch => {
return {
onGetGeneralInfo: bindActionCreators(getGeneralInfo, dispatch),
};
};
this is reducer
case GET_GENERAL_INFO_SUCCESS: {
const {payload} = action;
return {
...state,
generalInfo: payload,
};
}
this is action
export function getGeneralInfo(data) {
return {
type: GET_GENERAL_INFO,
payload: data,
};
}
export function getGeneralInfoSuccess(data) {
return {
type: GET_GENERAL_INFO_SUCCESS,
payload: data,
};
}
export function getGeneralInfoFail(data) {
return {
type: GET_GENERAL_INFO_FAIL,
payload: data,
};
}
and this is saga
export function* getGeneralInfoSaga() {
try {
const tokenKey = yield AsyncStorage.getItem('tokenKey');
const userId = yield AsyncStorage.getItem('userId');
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${tokenKey}`,
},
};
const response = yield call(
fetch,
`${API_GET_GENERAL_INFO}?id=${userId}`,
params,
);
const body = yield call([response, response.json]);
if (response.status === 200) {
yield put(getGeneralInfoSuccess(body));
} else {
yield put(getGeneralInfoFail());
throw new Error(response);
}
} catch (error) {
yield put(getGeneralInfoFail());
console.log(error);
}
}
the initial state in redux and state in component is an empty array.
so I want to GET data from API. and I push it to redux's state. then I
useState it. I want to use useEffect because I want to update state
when I PUT the data and update local state after update.
Ok, so I've gathered that you want fetch the data when the component mounts, and then store the fetched data into local state when it is populated. For this you will want to separate out the concerns into individual effect hooks. One to dispatch the data fetch once when the component mounts, the other to "listen" for changes to the redux state to update the local state. Note that it is generally considered anti-pattern to store passed props in local state.
const {onGetGeneralInfo, generalInfo} = props;
const [data, setData] = useState(generalInfo);
// fetch data on mount
useEffect(() => {
onGetGeneralInfo();
}, []);
// Update local state when `generalInfo` updates.
useEffect(() => {
setData(generalInfo);
}, [generalInfo, setData]);
in your useEfect you are setting generalInfo and it causes change in the dependency array of useEffect. So, it runs over and over:
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [generalInfo]);
try this instead:
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo); // or try to remove it if it is unnecessary based on below question.
}, []);
However, I don't understand why you have used setData(generalInfo); in useEffect when you have set it before. does it change in onGetGeneralInfo(); function?
Yow hook has or uses things that are not listed in the dependencies list
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [ onGetGeneralInfo, setData, generalInfo]);
Also let's remember that useEffect is call before the component mounts and after it mounts, so if you add a log it will be printed
I'm learning react query and the following code is working as expecting but I have this warning message:
React Hook useEffect has missing dependencies: 'code' and 'mutate'. Either include them or remove the dependency array.eslintreact-hooks/exhaustive-deps
But if I add 'code' and 'mutate' in the dependency array I have an infinite loop.
import React, { useState, useEffect } from "react";
import { useMutation } from "react-query";
import * as api from "../api/api";
const getQuery = () => {
const queryParams = new URLSearchParams(window.location.search);
return queryParams.get("code");
};
const Authentication = () => {
const [code] = useState(getQuery());
useEffect(() => {
if (code) {
mutate.mutate(code);
}
}, []);
const auth = async () => {
window.location.href = `https://www.betaseries.com/authorize?client_id=${process.env.REACT_APP_API_KEY}&scope=&redirect_uri=${process.env.REACT_APP_API_URL_CALLBACK}`;
};
const mutate = useMutation(api.access_token, {
onSuccess: (data) => {
localStorage.setItem("isAuth", data.data.access_token);
},
});
return <button onClick={auth}>Login</button>;
};
export default Authentication;
Short explanation of what I did:
User click on login button, he is redirected to the website to enter his login / password
const auth = async () => {
window.location.href = `https://www.betaseries.com/authorize?client_id=${process.env.REACT_APP_API_KEY}&scope=&redirect_uri=${process.env.REACT_APP_API_URL_CALLBACK}`;
};
After a success login he is redirected to my website with a url params ?code=xxx
I catch the code and use it to call a route that will provide me his access_token
useEffect(() => {
if (code) {
mutate.mutate(code);
}
}, []);
The mutate function itself is stable, but the object returned from useMutation is not. If you destruct, you can add it to your dependency array:
const { mutate } = useMutation(…)
You can directly pass code and mutate on the dependence array since your useEffect is depending on the change in variable code and mutate object it self. here is what you can try:
useEffect(() => {
if (code) {
mutate.mutate(code);
}
}, [code, mutate]);
I have this useEffect hook which does something on componentDidMount and want to use it in the end to update my redux store at componentWillUnMount.
const [ordersToCancel, dispatchCancelStatus] = useReducer(cancelReducer, []);
const cancelReducer = (state, action) => {
//implementation
}
useEffect(()=>{
//some Code
dispatchCancelStatus({ type: "type", state: state });
return async ()=> {
const data = ordersToCancel //ordersToCancel is an empty array here as it's default value not the updated one
//some code
const results = await api({params})
dispatch({ type: 'someType', data: data })
}
}, [])
As mentioned in code snippet, ordersToCancel get reset in cleanup function. I'm making sure this is getting updated. I have another useEffect hook with dependency of ordersToCancel and I can see its getting called and updating the array.
Is it the normal behavior that the state will get reset to default in cleanup function?
You can use useRef to keep an indirect reference to the ordersToCancel variable:
const [ordersToCancel, dispatchCancelStatus] = useReducer(cancelReducer, []);
const ordersToCancelRef = useRef();
ordersToCancelRef.current = ordersToCancel;
const cancelReducer = (state, action) => {
//implementation
}
useEffect(()=>{
//some Code
dispatchCancelStatus({ type: "type", state: state });
return async ()=> {
const data = ordersToCancelRef.current;
//some code
const results = await api({params})
dispatch({ type: 'someType', data: data })
}
}, [])
I have following structure in a react app:
<SomeProvider id={objectId}>
<SomeApp />
</SomeProvider>
This provider uses a useQuery to fetch an object from the backend. With useContext I make it accessible to all the components of the app:
const EventProvider = ({ id, children }) => {
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
// etc.
}
export const useSomething = () => {
// etc.
}
In a component I can have access to this object:
const Component = ({id}) => {
const { obj } = useSomething()
}
Until here all working. My question is, inside this component, I have a button that changes this object in the backend.
How can I fetch the obj again?
I can of course refresh the page, but this is the solution that I want to avoid. What I've tried so far is:
Try to use the useQuery again in the Component.
const Component = ({id}) => {
const { obj } = useSomething()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
}
But actually what I would like to do is trigger the query when a State variable changes:
const Component = ({id}) => {
const { obj } = useSomething()
const { isActivated } = useOtherHook()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
useEffect(() => {
// when isActivated changes, I would like to fetch the obj again
}, [isActivated])
}
If I use useQuery inside of useEffect, I get:
Hooks can only be called inside of the body of a function component
What is the best approach to solve this challenge?
The reason for the error messages you get is, that you can only use hooks in the body of your component, not inside of other functions that are not hooks, nor inside of conditions. You can read more about that here in the rules of hooks
As you can read in the documentation for useQuery here, there are multiple ways of keeping data up to date or refetching.
The easiest way of keeping data up to date would be to use the polling feature from apollo.
const { loading, error, data } = useQuery(QUERY, {
variables: input,
skip: !isActivated,
pollInterval: 500, // Update every 500ms
});
One way of refetching on demand would be to use the returned refetch function.
const { loading, error, data, refetch } = useQuery(/* ... */);
useEffect(() => { refetch() }, [isActivated])
Depending on your needs, you could also use the useLazyQuery hook. This does not fetch data on render but only on function call. This is most useful when you want to only request data when the input has been set or changed.
const [getData, { loading, data }] = useLazyQuery(QUERY);
useEffect(() => { getData({ variables: input }) }, []);
I am facing a problem with custom hook. I can't access states in the function. For example: data, error, loading
It is showing an error: "loading is not defined". I know that variables is out of scope but I want to use loading, error.
export const useLikeTrack = track => {
const { addFavoriteTrack } = useTrackContext();
const [success, setSuccess] = useState(false)
const likeTrack = (params) => {
const { data, error, loading } = useAxios({
axiosInstance: myApiAxiosInstance,
url: `tracks/${track["id"]}/likes`,
method: "POST"
});
}
useEffect(() => {
if (!loading && data) {
addFavoriteTrack(track);
setSuccess(true);
}
}, [loading, data]);
return { loading, error, success, likeTrack };
};
export default function TrackItem({ track }) {
const {success, loading, error, likeTrack} = useLikeTrack(track.id)
return (
<div className="flex">
<button className="" onClick={likeTrack}>Like
</button>
</div>
);
}
Can you help me fix it ? I am using useAxios from this: https://github.com/angelle-sw/use-axios-client
if (!loading && data) {
There is no loading nor data declared in that scope. They are inside another function called likeTrack()
To be more specific:
const likeTrack = (params) => {
const { data, error, loading } = useAxios({
// ...
}
}
those vars (data, error, loading) are not accessible outside of that function
As #Aprillion suggested in the comments, you should use the normal axios package to create a request in a handler, like so:
import axios from "axios";
...
const [loading, setLoading] = useState(false);
const [data, setData] = useState();
const likeTrack = (params) => {
setLoading(true);
myApiAxiosInstance.post(`tracks/${track["id"]}/likes`)
.then(response => {
setLoading(false);
setData(response.data)
})
}
...
However, if you really want to use a hook, try out the axios-hooks library instead. Beside the fact that it has way more recent npm-downloads than your package, you can call an axios request manually which is basically what you need here.
First, install the library with npm install axios axios-hooks
Then adjust your code like so:
import axios from "axios";
...
const [{ data, error, loading }, execute] = useAxios({
axiosInstance: myApiAxiosInstance,
url: `tracks/${track["id"]}/likes`,
method: "POST"
},
{
manual: true // This is important, otherwise your request would be fired automatically after your component mounted
});
const likeTrack = (params) => {
execute(); // execute the request manually
}
...
Note how I wrapped the useAxios return value with an array and added execute at the end. With execute() you can trigger the request manually. Also, don't forget to configure your request to only fire manually, as in the example.
source: https://github.com/simoneb/axios-hooks#example
It looks like you're passing track.id instead of track to your custom hook.