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 }) }, []);
Related
I want to make an API call with useSWR when navigation(route)'s params updates and store his value on a custom hook with contextAPI.
I have this component UsersPage that renders a list of users, it needs to have pagination, and when I click on some user, share his link based on his id and query requested (page, userId, nationality=nat, gender, etc).
I'm considering using useSWR for a better User Experience.
I tried
import { useUsers } from "../../hooks/useUsers";
import { useFetch } from "../../hooks/useFetch";
.
.
.
const { pageNumber, userId } = useParams();
const {
selectedGender,
nationality,
results,
showDetails,
page,
isAscending,
toggleIsAscending,
selectedUser,
formattedName,
formattedBirth,
getQuery,
} = useUsers();
useEffect(() => {
const users = useFetch(getQuery());
//then update users state on useUsers Context's hook and render data on Context's child nodes
}, [pageNumber]);
on custom hook useUsers the function getQuery is
const getQuery = () => {
const query = `&gender=${selectedGender}&nat=${nationality}&page=${page}&results=${results}`;
return query;
};
On useFetch hook I have:
import useSWR from "swr";
export function useFetch(query) {
const API_URL = process.env.REACT_APP_API_URL;
const fetcher = async (query) => {
const FormedURL = `${API_URL}${query}`;
const response = await fetch(FormedURL);
const data = await response.json();
return data.results;
};
const { data, error } = useSWR(query, fetcher);
return { users: data, error };
}
I received:
'React Hook "useFetch" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.'
Have someone done this before and share your experience/advice?
I would try something like this :
const { pageNumber, userId } = useParams();
const {
selectedGender,
nationality,
results,
showDetails,
page,
isAscending,
toggleIsAscending,
selectedUser,
formattedName,
formattedBirth,
getQuery,
} = useUsers();
const { get, response } = useFetch(getQuery())
const fetchUsers = async () {
const users = await get()
if (response.ok){
// sets users...
}
}
useEffect(() => {
fetchUsers()
}, [pageNumber]);
The way I cache data in Class component is like below :
1. make async API call in componentDidMount
2. get API response and dispatch data through redux
3. use response data by mapping state to prop
What I want to do is caching data right after you get API response with mapped redux state value
inside of useEffect in function component
(
It works on class component. but I'm wondering how should I make it work in function component)
export class MyClassComponent extends React.Component {
private readonly _myCachedData = {
'data1': {}
}
public async componentDidMount() {
await this.loadAsyncData();
}
private loadAsyncData() {
const { makeAPICall } = this.props
await makeAPICall();
return this._myCachedData.data1 = this.props.data1FromReduxConnect;
}
}
export const mapStateTopProps = (state) => {
const { data1FromReduxConnect } = state;
return data1FromReduxConnect;
}
...
What I have tried :
export const MyFunctionComponent = props => {
const { data1FromReduxConnect } = props;
const myCachedData = React.useRef();
const loadAsyncData = () => {
const { makeAPICall } = this.props
await makeAPICall();
return myCachedData.current = data1FromReduxConnect;
}
React.useEffect(()=> {
await loadAsyncData();
})
}
export const mapStateTopProps = (state) => {
const { data1FromReduxConnect } = state;
return data1FromReduxConnect;
}
I was only able to get the previous value ofdata1FromReduxConnect unlike class component did get updated value this.props.data1FromReduxConnect after API call
Not sure if I should just keep class component for it, or is there a way to deal with this issue!
Thanks!!
I don't think that is the right way to use the useRef hook. Similar to React's class components' createRef(), it is actually used to access the DOM in functional components.
If the HTTP request happens only once when MyFunctionComponent is initialised, we can use [] as the second argument in the useEffect hook which will cause this effect to be run only once. In addition, we will need to make use of useState hook to keep track of the component's state which is to be updated with the values from the redux store.
export const MyFunctionComponent = props => {
const { data1FromReduxConnect } = props;
const [ myData, setMyData ] = useState();
const loadAsyncData = async() => {
const { makeAPICall } = this.props
await makeAPICall();
}
useEffect(()=> {
async function getData() {
await loadAsyncData();
}
getData();
// do the rest to get and store data from redux
setMyData(data1FromReduxConnect);
}, [])
}
I am stuck at getting context data.
I have a context and a component which uses its data.
I need to get the updated data of context's variable on API call success in my component.
so How can I do that ?
Here what I have tried.
context.js
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
const [dataValue, setData] = useState([])
useEffect(() => {
const fetchMyData = async () => {
const dataValue = await getData(); // this is an API call
setData(dataValue)
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
state: {
dataValue
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
myComponent.js
import React, { useState, useContext } from 'react'
import context from './context'
import deleteAPI from './actionMethods'
function myComponent(props) {
const id= 10
const {state,actions} = useContext(context)
deleteAPI(id).then(res => {
if (res){
// what should I write here to get the updated Data from the context which will call an API to get the updated data.
}
})
}
Any help would be great.
Thank You.
As a generic example, one option is to fetch the data from the server when the app loads in the front-end. From there you can send requests to modify the server data and at the same time update your local version. Something like:
Fetch data and save it to the local store: [{id: 0, name: 'first'},{id: 1, name: 'second'}]
Modify the data sending a request to the server. For example deleting an item. id: 0
Once the server responds confirming the operation was successful you can modify that data in the local store. [{id: 1, name: 'second'}]
You can handle the data using a Redux store or a React Context. For example, using a Context:
export const ItemsContext = createContext([]);
export const ItemsContextProvider = props => {
const [items, setItems] = useState([]);
const deleteItem = id => {
deleteItemsAxios(id).then(() => {
setItems(items => items.filter(item => item.id !== id));
});
};
useEffect(() => {
const fetchItems = async () => {
const items_fetched = await fetchItemsAxios();
if (items_fetched) {
setItems(items_fetched);
} else {
// Something went wrong
}
};
fetchItems();
}, []);
return (
<ItemsContext.Provider
value={{
items,
deleteItem
}}
>
{props.children}
</ItemsContext.Provider>
);
};
We define a Component that will manage the data fetch. The data items are inside a state. When the Component mounts we fetch the items and save them in the state. If we want to delete an item we first call the corresponding fetch function. Once it finishes, if it was successful, we update the state and remove that item. We use React Context to pass the items data, as well as the deleteItem function, to any component that needs them.
Let me know if you need more explanation.
I am using useAxios hook to fetch data from Api then use add data to global state by Constate library.
However, I am getting error: "Maximum update depth exceeded ". So, how to make it work ? Also, what is best practice to handle asynchronous Api and global state in my use case ?
In index Component:
const {loading, error} = useGetFavoriteTracks()
const {state} = useTrackContext()
In useGetFavoriteTracks.js, fetch data then add favorite tracks to global state
export const useGetFavoriteTracks = params => {
const { data, error, loading } = useAxios({
axiosInstance: myApiAxiosInstance,
url: myApiEndPoint.FAVORITE_TRACKS
});
const { addFavoriteTracks } = useTrackContext();
addFavoriteTracks(data); //add favorite tracks to global state
return { loading, error };
};
TrackContext.js
const useTrack = params => {
const [state, dispatch] = useReducer(reducer, initialState);
const addFavoriteTracks = (data) => {
dispatch({
type: trackActionTypes.FAVORITE_TRACKS_FETCHED_SUCCESS,
payload: data
})
}
return {state, addFavoriteTracks}
}
export const [TrackProvider, useTrackContext] = constate(useTrack);
Rule of thumb: any direct or indirect call to setState or dispatch must not be fired in the process of rendering.
In useGetFavoriteTracks you must not unconditionally call addFavoriteTracks(data); like that. Put that call inside useEffect.
useEffect(() => {
if (!loading && data) addFavoriteTracks(data);
}, [loading, data])
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.