React query how to handle a search - reactjs

I'm just playing around with react query
I'm building sort of a simple github clone
I have to use useQuery twice one for the current user
as router param the other with a new search.
I ended up with:
const history = useHistory();
const currentUser: string = useRouterPathname();
const [user, setUser] = useState(currentUser);
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
setUser(data.search);
};
const { isLoading, error, data } = useGetUserData(user);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>An error has occurred: " + {error}</p>;
console.log(user, data);
Is it the right way?

The important part is probably that in useGetUserData, you put the user into the queryKey of react-query, something like:
const useGetUserData = (user) => useQuery(['users', user], () => fetchUserData(user))
so that you always refetch data when the user changes and users are not sharing a cache entry between them.
Something not react-query related though: The good thing about react-router is that you can also use it as a state manager - their hooks also subscribe you to changes, so there is no real need to duplicate that with local state:
const history = useHistory();
const currentUser: string = useRouterPathname();
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
};
const { isLoading, error, data } = useGetUserData(currentUser);
once you push to the history, all subscribers (via useParams or useLocation) will also re-render, thus giving you a new currentUser.
Lastly, I would recommend checking for data availability first rather than for loading/error:
const { error, data } = useGetUserData(user);
if (data) return renderTheData
if (error) return <p>An error has occurred: " + {error}</p>;
return <p>Loading...</p>
it obviously depends on your use-case, but generally, if a background refetch happens, and it errors, we still have data (albeit stale data) that we can show to the user instead. It's mostly unexpected if you see data on the screen, refocus your browser tab (react-query will try to update the data in the background then per default), and then the data disappears and the error is shown. It might be relevant in some cases, but most people are not aware that you can have data and error at the same time, and you have to prioritise for one or the other.

Related

How to manage global states with React Query

i have a project that's using Nextjs and Supabase. I was using context API and now i'm trying to replace it for React Query, but i'm having a hard time doing it. First of all, can i replace context completely by React Query?
I created this hook to get the current user
export const getUser = async (): Promise<Profile> => {
const onFetch = await supabase.auth.getUser();
const userId = onFetch.data.user?.id;
let { data, error } = await supabase
.from("profiles")
.select()
.eq("id", userId)
.single();
return data;
};
export const useUser = () => {
return useQuery(["user"], () => getUser());
};
I'm not sure how to trigger it. When i was using context i was getting the user as this. If data, then it would redirect to the HomePage
let { data, error, status } = await supabase
.from("profiles")
.select()
.eq("id", id)
.single();
if (data) {
setUser(data);
return true;
}
Since i was getting the user before redirecting to any page, when i navigated to profile page, the user was already defined. How can i get the user before anything and keep this state? I suppose that once the user is already defined, at the profile component i can call useUser and just use it's data. But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
const { data, isLoading } = useUser();
But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
Once data is fetched when you call useUser, it will not be removed anymore (unless it can be garbage collected after it has been unused for some time). So if you do a client side navigation (that is not a full page reload) to another route, and you call useUser there again, you should get data back immediately, potentially with a background refetch, depending on your staleTime setting).
If you're still getting undefined, one likely error is that you are creating your QueryClient inside your app and it thus gets re-created, throwing the previous cache away. You're not showing how you do that so it's hard to say. Maybe have a look at these FAQs: https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable

React: Query, delay useReducer

I've been scratching my head around this one for quite some time now, but I'm still not sure what to do.
Basically, I'm pulling data from a database via useQuery, which all works well and dandy, but I'm also trying to use useReducer (which I'm still not 100% familiar with) to save the initial data as the state so as to detect if any changes have been made.
The problem:
While the useQuery is busy fetching the data, the initial data is undefined; and that's what's being saved as the state. This causes all sorts of problems with regards to validation amd saving, etc.
Here's my main form function:
function UserAccountDataForm({userId}) {
const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
const rows = React.useMemo(() => {
if (userData) { /* Process userData here into arrays */ }
return [];
}, [isLoadingUserData, userData]); // Watches for changes in these values
const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
const { submit, /* updateFunctions here */ } = methods;
if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues
return (
<>
Render stuff here
*isDirty* is used to detect if changes have been made, and enables "Update Button"
</>
)
}
Here's useUserData (responsible for pulling data from the DB):
export function useUserData(user_id, column = "data") {
const query = useQuery({
queryKey: ["user_data", user_id],
queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
staleTime: Infinity,
});
}
return { query }
And here's the reducer:
function userDataFormReducer(state, action) {
switch(action.type) {
case "currency":
return {... state, currency: action.currency}
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
default:
return;
}
}
function useUserDataForm(handleSubmit, userData) {
const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.
const updateFunction = (type, value) => { // sample only
dispatch({type: type, value: value});
}
}
export { useUserDataForm };
Compounding the issue is that it doesn't always happen. The main form resides in a <Tab>; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
The quickest "fix" I can think of is to NOT set the initial data (by not calling the reducer) while useQuery is running. Unfortunately, I'm not sure this is possible. Is there anything else I can try to fix this?
Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
This is likely to be expected because useQuery will give you data back from the cache if it has it. So if you come back to your tab, useQuery will already have data and only do a background refetch. Since the useReducer is initiated when the component mounts, it can get the server data in these scenarios.
There are two ways to fix this:
Split the component that does the query and the one that has the local state (useReducer). Then, you can decide to only mount the component that has useReducer once the query has data already. Note that if you do that, you basically opt out of all the cool background-update features of react-query: Any additional fetches that might yield new data will just not be "copied" over. That is why I suggest that IF you do that, you turn off the query to avoid needless fetches. Simple example:
const App = () => {
const { data } = useQuery(key, fn)
if (!data) return 'Loading...'
return <Form data={data} />
}
const Form = ({ data }) => {
const [state, dispatch] = useReducer(userDataFormReducer, data)
}
since the reducer is only mounted when data is available, you won't have that problem.
Do not copy server state anywhere :) I like this approach a lot better because it keeps server and client state separate and also works very well with useReducer. Here is an example from my blog on how to achieve that:
const reducer = (amount) => (state, action) => {
switch (action) {
case 'increment':
return state + amount
case 'decrement':
return state - amount
}
}
const useCounterState = () => {
const { data } = useQuery(['amount'], fetchAmount)
return React.useReducer(reducer(data ?? 1), 0)
}
function App() {
const [count, dispatch] = useCounterState()
return (
<div>
Count: {count}
<button onClick={() => dispatch('increment')}>Increment</button>
<button onClick={() => dispatch('decrement')}>Decrement</button>
</div>
)
}
If that works is totally dependent on what your reducer is trying to achieve, but it could look like this:
const App = () => {
const { data } = useQuery(key, fn)
const [state, dispatch] = useReducer(userDataFormReducer)
const currency = state.currency ?? data.currency
}
By keeping server state and client state separately, you'll only store what the user has chosen. The "default values" like currency stay out of the state, as it would essentially be state duplication. If the currency is undefined, you can still choose to display the server state thanks to the ?? operator.
Another advantage is that the dirty check is relatively easy (is my client state undefined?) and resets to the initial state also just mean to set the client state back to undefined.
So the actual state is essentially a computed state from what you have from the server and what the user has input, giving precedence to the user input of course.

In react-native, how to correctly order data-fetching and useState that depends on the data

I am new to react-native so apologies if this is a dumb question.
I have the following code (only showing the relevant part):
const Alarms = () => {
var { loading, error, data } = useQuery(ALARMS_QUERY) // this line fetches data with Apollo Client
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error :(</Text>;
const [query, setQuery] = React.useState("");
const [displayed, setDisplayed] = React.useState(data.alarms.items)
...
}
query and setQuery are used to manage the state of the query that user types into a search bar.
displayed and setDisplayed are used to manage the state of the filtered items that will appear as a FlatList down below the search bar.
This gives an error:
Rendered more hooks than during the previous render
And I understand that it's because I put state management after two if-statements. However, how to go around this? I can't put state management before if-statements since it depends on the existence of data.
Could someone kindly help me? Thank you!
const Alarms = () => {
var { loading, error, data } = useQuery(ALARMS_QUERY) // this line fetches data with Apollo Client
const [query, setQuery] = React.useState("");
const [displayed, setDisplayed] = React.useState("")
useEffect(()=> {
if(data){
setDisplayed(data.alarms.items)
}
}, [data])
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error :(</Text>;
...
}
You can set the displayed state an empty string first, if data is coming asynchronously, you can set it with useEffect later.

Unexpected result from apollo react's useQuery method

For a project, where i've implemented authentication by running a GraphQL query inside a AuthenticationProvider from a context, I noticed the query is fetching data twice.
const AuthenticationProvider: FC = props => {
const {
loading,
data
} = useQuery(MeQuery)
if (loading) return null
return <AuthenticationContext.Provider value={{user: data?.me || null}} {...props} />
}
However the query runs perfect, I still wanted to know why it fetches the data twice. I did some googling, and came across this issue, where this answer was provided. I tried the same thing, with the skip option, based if the data is loaded.
const [skip, setSkip] = useState(false)
const {
loading,
data
} = useQuery(MeQuery, { skip })
useEffect(() => {
if (!loading && data?.me) {
setSkip(true)
}
}, [loading, data])
// ...
But when logging in, it stopped working.
const useLoginMutation = () => useMutation(LOGIN_QUERY, { update: (cache, { data }) => {
if (!data) {
return null
}
cache.writeQuery({ query: MeQuery, data: { me: data.login } })
}
})
The cache still get's updated with the right values, but doesn't retrieve the user anymore (null).
const { user } = useContext(AuthenticationContext)
What am I missing here? It seems the query did run and fetched the correct data.
You don't need to use context when you are using apollo useQuery. If you make a query first, then the data fetched will be stored in the cache. You can directly access the data from the cache for the second you run the query. Since useQuery has default fetchPolicy cache-first. Mean its check in the cache first, if no query made before it makes a network request.
If you want to avoid network loading. You can make a top-level component AuthWrapper.
const useUserQuery = () => useQuery(ME_QUERY);
const AuthWrapper = ({children}) => {
const {loading, data} = useUserQuery();
if(loading) return null
return children;
}
export GetUsetInThisComponent = ({}) => {
// Since we have fetched the user in AuthWrapper, the user will be fetched from the cache.
const {data} = useUserQuery();
// No you can access user from data?.user
// Rest of the application logic
}
// Wrap the component like this to avoid loading in the children components
<AuthWrapper>
<GetUserInThisComponent />
</AuthWrapper>

React JS Not Read and Write to localStorage in useEffect No Errors

Ultimate goal is to store the JSON data. That way, if the same github user is sent to the GitHubUser component, instead of making a fresh call to the API, it should load the details from the local storage, preventing a network call.
Key Points about the problem.
do a simple fetch from github public api (no issues, working fine)
store the data to local storage with the github username as key (not working)
retrieve the data from local storage by providing a github username as key (not working)
display json data after render is complete using useEffect (working fine)
I get no errors of any kind with localStorage but nothing gets saved. I have tried this on both Firefox and Edge. The network call happens on every change of login, for the same user, which it should not.
Further, this code is from a textbook I am following, and this is a exact copy from the page that discusses fetch and useEffect. The author goes on to explain that it should work and so far the book has been correct with no errors.
I have put the code in a sandbox here - https://codesandbox.io/s/bold-http-8f2cs
Also, the specific code below.
import React, { useState, useEffect } from "react";
const loadJSON = key =>
key && JSON.parse(localStorage.getItem(key));
const saveJSON = (key, data) =>
localStorage.setItem(key, JSON.stringify(data));
function GitHubUser({ login }) {
const [data, setData] = useState(
loadJSON(`user:${login}`)
);
useEffect(() => {
if (!data) return;
if (data.login === login) return;
const { name, avatar_url, location } = data;
saveJSON(`user:${login}`, {
name,
login,
avatar_url,
location
});
}, [data]);
useEffect(() => {
if (!login) return;
if (data && data.login === login) return;
fetch(`https://api.github.com/users/${login}`)
.then(response => response.json())
.then(setData)
.catch(console.error);
}, [login]);
if (data)
return <pre>{JSON.stringify(data, null, 2)}</pre>;
return null;
}
//Jay-study-nildana
//MoonHighway
export default function App() {
return <GitHubUser login="Jay-study-nildana" />;
}
Note : I get a couple of warnings related to useEffect but I have already isolated that they are not the issue but I dont think they are the problem. it simple tells me not to use a dependency array since there is only one element for both useEffects. I am using the array on purpose.
Update 1
One thing I noticed is, in developer tools, nothing is getting stored in Local Storage after a successfull call to the API. So, right now, I am thinking, saving is not working. Unless I get that working and see the stored data in developer tools, I wont know if load is working or not.
First, if the initial state is the result of some computation, you may provide a function instead, which will be executed only on the initial render:
// instead of this
const [data, setData] = useState(
loadJSON(`user:${login}`)
);
// you better have this
const [data, setData] = useState(() => {
return loadJSON(`user:${login}`);
});
Second, you can achieve what you need with this single useEffect:
const [data, setData] = useState(() => { return loadJSON(`user:${login}`); });
useEffect(() => {
if (!data) {
fetch(`https://api.github.com/users/${login}`)
.then((response) => response.json())
.then((val) => {
saveJSON(`user:${login}`, val); // put data into localStorage
setData(val); // update React's component state
})
.catch(console.error);
}
});
if (data) return <pre>{JSON.stringify(data, null, 2)}</pre>;
return <div>no data</div>;
You will get your data in localStorage. Don't forget that you need to use key user:${login} if you need to get it from there.

Resources