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.
Related
So I am using a context provider to give the base data to my app for example: [{id: "a", name: "a"}].
Now I have a component that required the data portion of this object, I check if this data property is not yet there, I get it from my api, fill it and then it should not have to recall the api to get it again.
My provider:
import { createContext, useState, useCallback, useMemo, useContext } from "react";
import axios from "axios";
export const StockContext = createContext();
export const useStock = () => useContext(StockContext);
export const StockProvider = ({ children }) => {
const [stocks, setStocks] = useState();
const getDataFromStock = useCallback(
async ({ id }) => {
let method = "GET";
let url = `${config.base_url}data/${id}`;
try {
const { data: response } = await axios({ method, url });
const { succes, data, error } = response;
let updated = stocks;
updated.find((s) => s.id === id).data = data;
console.log("set stocks", updated);
setStocks(updated);
}
return succes;
}
},
[stocks]
);
const value = useMemo(
() => ({
getDataFromStock,
stocks,
}),
[getDataFromStock, stocks]
);
return <StockContext.Provider value={value}>{children}</StockContext.Provider>;
};
Note: I removed some error handling for simplicity sake.
After the function getDataFromStock with the id is called. The stocks object should look like this: [{id: "a", name: "a", data: [{id: "c", ...}]}].
Now I have my component to show the details (data) from this object. It first checks if it is not already in the 'stocks' object and if not gets it.
export default function StockDetailPage() {
const { id } = useParams();
const [currentStock, setCurrentStock] = useState({});
const { stocks, getDataFromStock, testStocks } = useContext(StockContext);
// TODO find why data keep disappearing
useEffect(() => {
const getData = async () => {
if (!stocks.find((s) => s.id === id).data) {
console.log("getting");
await getDataFromStock({ id });
// TODO Indication on loading?
} else {
console.log("not getting");
}
};
getData();
}, [getDataFromStock, id, stocks]);
return (
<div>
Stock: {currentStock?.owner?.username}
{stocks
?.find((s) => s.id === id)
.data?.map((p) => (
<ProductPreview key={p.product_id} {...p} />
))}
</div>
);
}
Now if I look at my react debugger, I see the state of the stocks has this data object, but once I navigate away from the details, this property seems to disappear. Now how could I implement this in the right way or fix the problem?
Kind regards
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
Can someone please tell me the equivalent code using hooks for the following:
componentDidMount() {
const { match: { params } } = this.props;
axios.get(`/api/users/${params.userId}`)
.then(({ data: user }) => {
console.log('user', user);
this.setState({ user });
});
}
The exact functionality to match your class component into a functional component with hooks would be the following:
import * as React from "react";
import { useParams } from "react-router-dom";
const Component = () => {
const { userId } = useParams();
const [state, setState] = React.useState({ user: null });
React.useEffect(() => {
axios.get(`/api/users/${userId}`)
.then(({ data: user }) => {
console.log('user', user);
setState({ user });
});
}, []);
}
React.useEffect(() => {}, []) with an empty dependency array essentially works the same way as the componentDidMount lifecycle method.
The React.useState hook returns an array with the state and a method to update the state setState.
References:
https://reactjs.org/docs/hooks-state.html
https://reactjs.org/docs/hooks-effect.html
As an aside, and pointed out by #Yoshi:
The snippet provided is error prone, and the "moving to hooks" snippet will have the same errors that occur in the example. For example, as the request is in componentDidMount, if the userId changes it won't trigger a fetch to get the user data for the userId. To ensure this works in the hook, all you need to do is provide the userId in the dependency array in the useEffect...
const latestRequest = React.useRef(null);
React.useEffect(() => {
latestRequest.current = userId;
axios.get(`/api/users/${userId}`)
.then(({ data: user }) => {
if (latestRequest.current == userId) {
setState({ user });
}
});
}, [userId]);
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.
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>
)
}