I'm building a small ToDo list app with React Apollo and GraphQL. In order to add a new ToDo item I click "Add" button that redirects me to a different URL that has a form. On form submit I perform a mutation and update the cache using update function. The cache gets updated successfully but as soon as I return to the main page with ToDo list, the component triggers an http request to get the ToDo list from the server. How do I avoid that additional request and make ToDoList component pull data from the cache ?
My AddToDo component:
const AddToDo = () => {
const { inputValue, handleInputChange } = useFormInput();
const history = useHistory();
const [addToDo] = useMutation(ADD_TODO);
const onFormSubmit = (e) => {
e.preventDefault();
addToDo({
variables: { title: inputValue },
update: (cache, { data: { addToDo } }) => {
const data = cache.readQuery({ query: GET_TODO_LIST });
cache.writeQuery({
query: GET_TODO_LIST,
data: {
todos: [...data.todos, addTodo],
},
});
history.push("/");
},
});
};
return (
...
);
};
And ToDoList component
const ToDoList = () => {
const { data, loading, error } = useQuery(GET_TODO_LIST);
if (loading) return <div>Loading...</div>;
if (error || !loading) return <p>ERROR</p>;
return (
...
);
};
Works as expected.
Why unecessary? Another page, new component, fresh useQuery hook ... default(?) fetchPolicy "cache-and-network" will use cached data (if exists) to render (quickly, at once) but also will make request to be sure current data used.
You can force "cache-only" but it can fail if no data in cache, it won't make a request.
Related
Here is my setup:
const ParentComponent = ({ post_id }) => {
const { data: post } = useGetPost(post_id); // custom hook with useQuery under the hood
return (
<div>
{post.status === 'published' && (
<div className="margin-bottom">
<ChildComponent post_id={post_id} />
</div>
)}
</div>
);
};
const ChildComponent = ({ post_id }) => {
const { data: post } = useGetPost(post_id);
return <div>{post.title}</div>;
};
Whenever i change post status to 'published' – React Query spawns two getPost requests, first of which, for some reason, returns previous post status and second returns the right one. Problem is gone if i move post.status === 'published' check to ChildComponent, but it's not an option because of ChildComponent wrapper element ("margin-bottom") inside ParentComponent. Is there something i don't get?
UPDATE:
useGetPost hook:
export const useGetPost = (post_id) => {
return useQuery<IDiscussion, Error>(
['posts', post_id],
async () =>
await axios
.get(`/api/posts/${post_id}`)
.then((response) => response.data),
);
};
And my mutation function:
export const useUpdatePost = (post_id) => {
const queryClient = useQueryClient();
return useMutation(
async (data) => await axios.put(`/api/posts/${post_id}`, data),
{
onMutate: (data) => {
// updating react query cache before actual save
queryClient.setQueryData(['posts', post_id], (oldData) => ({
...oldData,
data,
}));
},
onSuccess: () => {
queryClient.refetchQueries(['posts', post_id]);
},
},
);
};
ChildComponent mounts after you manually set new query data in "onMutate". The query refetches on mount (default behavior), but mutation is not yet finished, so you see the old status. Then, on mutation success query refetches again and you see updated status.
That is why if your ChildComponent gets mounted unconditionally, you see only one request. You can just get rid of onMutate function and refetch the query only once, after mutation success.
I have a component:
// MovieOverview.tsx
const MovieOverview = () => {
const [rerender, setRerender] = useState(false);
const {loading, error, data} = useQuery(resolvers.queries.ReturnAllMovies);
console.log('data: ', data);
let movies: IMovie[] = data?.movies;
useEffect(() => {
if (movies) {
console.log('movieVar: ', movieVar());
movies = [...movies, movieVar()];
}
setRerender(!rerender);
}, [useReactiveVar(movieVar)]);
if (loading) return <p>loading</p>;
if (error) return <p>Error! ${error.message}</p>;
return (
<div>
{movies.length}
</div>
);
};
In this component I have a useQuery that returns an array of objects (movies).
I have a useEffect that should add a object (movie) from my cache to the movies array when the movieVar is changed.
There's also a useState that updates the template.
My cache:
// cache.tsx
import {InMemoryCache, ReactiveVar, makeVar} from '#apollo/client';
import {IMovie} from './movieseat';
export const cache: InMemoryCache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
movies: {
read() {
return movieVar();
},
},
},
},
},
});
export const movieVar: ReactiveVar<IMovie> = makeVar<IMovie>({id: 1, original_title: '', backdrop_path: '', poster_path: '', release_date: ''});
And finally I have a component that adds a movie:
// addMovie.tsx
const [addMovieRes] = useMutation(resolvers.mutations.AddMovie);
addMovieRes({variables: {
original_title: movie.original_title,
tmdb_id: movie.id,
poster_path: movie.poster_path,
}});
movieVar(movie);
When I add a movie, the addMovie component stores the movie in the database, the added movie is placed in the reactive variable: movieVar. In my MovieOverview component the useEffect is triggered and the passed in movie object shows up as expected. The problem is that the useQuery doesn't do anything.
On the initial component load I can see:
But when I add a movie that ReturnAllMovies query is not called again:
Might have resolved it, looks good so far. So there's a refetch option.
Refetching enables you to refresh query results in response to a particular user action, as opposed to using a fixed interval.
SO I updated my useQuery:
const {loading, error, data, refetch} = useQuery(resolvers.queries.ReturnAllMovies);
And added the refetch callback (?) to the useEffect:
useEffect(() => {
if (movies) {
console.log('movieVar: ', movieVar());
movies = [...movies, movieVar()];
}
refetch();
}, [useReactiveVar(movieVar)]);
Now the useQuery is fired again and the data is updated.
I am new to Apollo and I am missing something.
I have a query to get the currently logged in user that I run when the page load and it's executed from one of my very top components (the same one that contains also the matching logic for react-router):
export const GET_USER = gql`
query {
me {
id
email
}
}
`
Then very nested in the DOM tree I have the login and logout buttons, both of them trigger mutations that are going to set or unset the session for the current user.
But... how do I update the state at the top of the app at this point?
I have read this blog post that suggests to wrap Apollo Hooks into other custom hooks:
const useAuth = () => {
const { data: getUserData } = useQuery(GET_USER)
const [login, { data: loginData } = useMutation(LOGIN)
const [logout, { data: logoutData } = useMutation(LOGOUT)
// Should I find out here if I have a user id or not?
// It's doable, but not clean
return { login, logout, userId }
}
It's very unintuitive to me to make requests in this way, it was easy for me to understand to have these side effects in Redux or MobX actions... hence I am moving to use the Apollo client directly from there, even if it's not the suggested solution from the Apollo docs.
What am I not getting right?
It will look something like below. The important part is that the mutation calls update function once it receives a response. That is the location where you manually update the cache. The goal would to replace the value of "me" that is in the apollo cache. The satisfying part is that once you get this working, the data retrieved from useQuery will automatically update (hence rerendering components using this hook). Here is the link to the documentation.
const useAuth = () => {
const { data: getUserData } = useQuery(GET_USER)
const [mutateLogin] = useMutation(LOGIN)
const [mutateLogout] = useMutation(LOGOUT)
function login(..args) {
mutateLogin({
variables: args,
update(proxy, {data}) {
proxy.writeQuery({
query: GET_USER,
data: {
me: (GET USER FROM data)
}
})
}
})
}
function logout() {
mutateLogout({
update(proxy, {data}) {
proxy.writeQuery({
query: GET_USER,
data: {
me: null
}
});
}
})
}
return { login, logout, userId }
}
In my app, I use apollo-cache-inmemory to manage the app state. My app is a single source of truth state, it seems like redux. When you want to manipulate data in Apollo state, you should use useQuery to query a client's data, use useMutation to update Apollo state. You can read more about how to interact with cached data in Apollo here.
This is an example:
In client.js:
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
const client = new ApolloClient({
link: new HttpLink(),
cache: new InMemoryCache(),
resolvers: {
Mutation: {
setSearchText : (_, { searchValue }, {cache}) => {
cache.writeData({
data: {
searchText: searchValue,
},
})
}
}
}
});
const initialState = {
searchText: ''
}
cache.writeData({ data: initialState})
In SearchBox.js
import React from 'react'
import { useQuery, useMutation } from '#apollo/react-hooks'
import gql from 'graphql-tag'
const SET_SEARCH_TEXT = gql`
mutation SetSearchText($searchValue: String!) {
setSearchText(searchValue: $searchValue) #client
}
`
const GET_SEARCH_TEXT = gql`
query SearchText {
searchText #client
}
`
const SearchBox = () => {
const { data: searchTextData, loading, error } = useQuery(GET_SEARCH_TEXT)
const [setSearchText] = useMutation(SET_SEARCH_TEXT)
const [value, setValue] = React.useState(searchTextData.searchText || '')
handleChange = (e) => {
setValue(e.target.value)
}
onSubmit = () => {
setSearchText({variables: { searchValue: value }}).then(data => console.log(data))
}
return (
<div>
<input type="text" value={value} onChange={handleChange} />
<button onClick={onSubmit}>Submit</button>
</div>
)
}
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 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.