How to share data across multiple components with react-query useMutation - reactjs

I'm looking for a solution to share data across a React-Query mutation without having to create my own internal state or context.
I created a custom hook that takes care of the API call.
myData.ts
const useDataMutation = () => useMutation('MY_DATA_MUTATION', postData);
Then, I use my custom hook in different components.
Component1 is in charge of mutating. The response data will be available in data once the mutate is successful.
Component1.tsx
const { mutate, data } = useDataMutation();
useEffect(() => mutate('some_data'), []);
In another nested component, I want to access the data which came back from the response. But I don't want to pass down the data to 3-4 layers of components. And I wanted to avoid using a Context to access this data.
What I want is something like this:
Component2.tsx
const { data } = useDataMutation();
console.log({ data }); // log data once available.
But in this example, the data from Component2.ts is always undefined.
Is there a simple way to achieve something like this?
Thank you.

at the moment, mutations don't share data across instances like queries do. There is an open issue about it, and contributions are welcome.

If you're using #apollo/client then you can read the previously fetched data directly from the cache.
import Query from './query.ts'
const { todo } = client.readQuery({
query: Query,
variables: {}
})
That will not fetch the data from your server again and instead fetch it from the apollo cache. So you can fetch it using the hook or in the parent component and then 5 levels down you can just pull it again from the cache using the same hook.
When you're using a mutation it will update data in the cache if the properties in the query is the same as before, so if you have a query for getUser and a mutation for updateUser I think the mutation should automatically update the getuser cache data if the data aligns with the mutation data. I'm not sure about this.

Related

Is there any way we can use the data cached by apollo after a query similar to React's useState? Will there be double store of data if I use useState?

I am using a GraphQL as an API for one my NextJS application. I want to manipulate the data queried using filters and other client interacting activities. The only way for me to accomplish that is by using React' useState() but I think using it will just be redundant as GraphQL also by default stores the queried data. So, Is there any way I can use cached data of Apollo GraphQL or I am misleading something here. I am new to Apollo Grpahql.
const GET_MOVIES = gql`
query GetMovies{
movies{
id
name
genre
yearReleased
rating
}
}
`
const {data, loading, error, refetch} = useQuery(GET_MOVIES);
const [allMovies, setAllMovies] = useState(data);
If you know your data is already cached, you can just query the cache using fetchPolicy: 'cache-only' - that will not hit the network. For me that's the simplest because the API is the same as getting data from the server.
Alternatively you can read/write to the cache yourself directly. [docs]

Accessing cached data in RTK Query(new)

Imagine we have 3 component.
first is index which is parent.
second is filter component and third one is table component.
I used mutations for filter some data and show them in table.
In filter component I did this:
const [filterSomeData] = useFilterSomeDataMutation();
const data = filterSomeData(myFilter);
Now I need to access data in table component.
Redux toolkit query with every request cache the result , how can I access that?
Generally: If you are receiving data from the server without triggering a change on the server, you should be using a query, not a mutation. Yes, you can do POST requests with Queries and the syntax is 100% the same as with mutations.
Then you should be using that useQuery hook in all components that need that data, with the same argument as you passed in initially. That means if you have something like a filter, that you should either pass that filter in by props (by lifting the filter state up to a common parent) or keeping that filter in a Redux slice and getting it from Redux before calling your query hook.
Since you are calling that useQuery hook with the same argument in multiple components, it will not make multiple requests, but reuse the response of the first request.
What you need is api.endpoints.foo.useLazyQuery() or api.useLazyFooQuery(). This way you can assess the last fetched data on that endpoint.
Read more from the official doc: https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#usequerystate
As in RTK documentation is explained, the proposed solution is calling useQuery hooks with the same arguments as before it called. It retrieves data from cached enpoints, But as I need too many arguments to send to query hook, and I should send it to other component, so I prefer to use the store object to access data from endpoints as below:
const store = useStore();
const cachedQueries = store.getState().dashboardApi.queries;
Then I made an object with endpoint's name as key and their data as value:
let dashboardResult: { [key: string]: any } = {};
Object.values(cachedQueries).forEach((item: any) => {
dashboardResult = {
...dashboardResult,
...{ [item?.endpointName]: item?.data?.data }
}
});

react-query: is there a callback that get trigger regardless of whether getQuery is cached or not?

I want to do some side effects like setState and update context after the data is fetched. However, the onSuccess will not be executed when the data is in cache. Also useEffect doesn't work because if the data is cached, it doesn't change from undefined to the real data. Therefore it doesn't get trigger either. What's the best way of doing this? Thanks
My usecase is to extract some values from the data returned from useQuery and set a new state on those.
usually, they’re shouldn’t be a need to be a need to copy state from react-query into local state. This will just lead to duplication of the source of truth. It is best to keep them separated, so that you can also profit from background updates.
If you want to transform the data or subscribe to parts of the data, use the select option of useQuery:
const { data } = useQuery(key, fn, { select: data => data.map(...) })
Alternatively, you can compute some new data depending on the returned data with useMemo, e.g.:
const { data } = useQuery(...)
const articles = useMemo(() => data?.map(...), [data])
// work with articles from here on
You can also put that nicely in a custom hook.

Synchronously read from the store?

So I'm using Apollo for my app's state, and am so far a little taken aback there's no equivalent to mapStateToProps or something.
As far as I understand, to have any data globally accessible in my store, once I get the data, I need a query to write the data to the store, then another query in my other component to go and get it.
By this point, the other component has very much mounted and rendered, so content just sort of flickers in and out.
In Redux, I can just add new data to the store in my reducers, then anything that's connected with mapStateToProps has access to it.
Is there an equivalent? Or does everything need to go through asynchronous queries? Does anyone else kind of find this an enormous pain?
For example, in one component I'm getting some invitation data:
this.props.client.query({
query: REQUEST_ACTION_DATA,
variables: {
id: actionData.id,
type: actionData.type
}
}).then(data => {
this.props.client.writeQuery({query: GET_ACTION_DATA, data: {
action: {
type: data.data.actionData.type,
object: data.data.actionData.invitation,
__typename: 'ActionDataPayload'
}
}})
this.props.history.push('/auth/register')
})
... then in my other component I have this:
componentWillMount() {
const authToken = localStorage.getItem(AUTH_TOKEN);
if (authToken) {
this.props.history.push('/')
}else{
this.props.client.query({
query: GET_ACTION_DATA
}).then(data => {
if(data.data && data.data.action && data.data.action.type == 'invite'){
this.setState({
invitation: data.data.action.object
})
}
console.log(data)
})
}
}
ignoring the fact that it's hugely unwieldy to write all this for something so simple, is there just a way to access store data without having to wait around?
The graphql higher order component from react-apollo is a supercharged connect from redux. It will do the following:
When the component mounts it will try and execute the query on the Apollo cache. You can think of the query as a DSL for selecting data not only from the server but also from the store! If this works the component is pretty much instantly filled with the data from the store and no remote requests are executed. If the data is not available in the store it triggers a remote request to receive the data. This will then set the loading property to true.
The Apollo cache is a normalised store very similar to your redux store. But thanks to the well thought through limitations of GraphQL the normalisation can happen automatically and you don't have to bother with the structure of the store. This allows the programmer to forget about stores, selectors and requests altogether (unless you are heavily performance optimising your code). Welcome to the declarative beauty of GraphQL frontend frameworks! You declare the data you want and how it gets there is fully transparent.
export default graphql(gql`{ helloWorld }`)(function HelloWorld({ data }) {
if (data.loading) {
return <div>Loading the simple query, please stand by</div>;
}
return <div>Result is: {data.helloWorld}</div>;
});
No selector, no denormalisation happening. This is done by Apollo Client! You will get updated data when the cache updates automatically similar to Redux.
No componentWillMount checks for data is in store already and triggering request actions. Apollo will make the checks and trigger queries.
No normalisation of response data (also done by Apollo Client for you)
If you do any of the above you are probably using Apollo Client wrongly. When you want to optimise your queries start here.

Where should I load data from server in Redux + ReactJS?

For example I have two components - ListOfGroupsPage and GroupPage.
In ListOfGroupsPage I load list of groups from the server and store it to the state.groups
In route I have mapping like ‘group/:id’ for GroupPage
When this address is loaded, the app shows GroupPage, and here I get the data for group from state.groups (try to find group in state via id).
All works fine.
But if I reload page, I'm still on page /group/2, so GroupPage is shown. But state is empty, so the app can't find the group.
What is the proper way to load data in React + Redux? I can see this ways:
1) Load all data in root component. It will be very big overhead from traffic side
2) Don't rely on store, try to load required data on each component. It's more safe way. But I don't think that load the same data for each component - it's cool idea. Then we don't need the state - because each component will fetch the data from server
3) ??? Probably add some kind of checking in each component - first try to find required data in store. If can't - load from the server. But it requires much of logic in each component.
So, is there the best solution to fetch data from server in case of usage Redux + ReactJS?
One approach to this is to use redux-thunk to check if the data exist in the redux store and if not, send a server request to load the missing info.
Your GroupPage component will look something like
class GroupPage extends Component {
componentWillMount() {
const groupId = this.props.params.groupId
this.props.loadGroupPage(groupId);
}
...
}
And in your action...
const loadGroupPage = (groupId) => (dispatch, getState) => {
// check if data is in redux store
// assuming your state.groups is object with ids as property
const {
groups: {
[groupId]: groupPageData = false
}
} = getState();
if (!groupPageData) {
//fetch data from the server
dispatch(...)
}
}
I recommend caching the information on the client using localstorage. Persist your Redux state, or important parts of it, to localstorage on state change, and check for existing records in localstorage on load. Since the data would be on the client, it would be simple and quick to retrieve.
The way I approach this is to fetch from the server straight after the store has been created. I do this by dispatching actions. I also use thunks to set isFetching = true upon a *_REQUEST and set that back to false after a *_SUCCESS or *_FAILURE. This allows me to display the user things like a progress bar or spinner. I think you're probably overestimating the 'traffic' issue because it will be executed asynchronosly as long as you structure your components in a way that won't break if that particular part of the store is empty.
The issue you're seeing of "can't get groups of undefined" (you mentioned in a comment) is probably because you've got an object and are doing .groups on it. That object is most likely empty because it hasn't been populated. There are couple of things to consider here:
Using ternary operators in your components to check that someObject.groups isn't null; or
Detailing in the initialState for someObject.groups to be an empty array. That way if you were to do .map it would not error.
Use selectors to retrieve the list of groups and if someObject.groups is null return an empty array.
You can see an example of how I did this in a small test app. Have a look at specifically:
/src/index.js for the initial dispatch
/src/redux/modules/characters.js for the use of thunks
/src/redux/selectors/characters.js for the population of the comics, series, etc. which are used in the CharacterDetails component

Resources