I'm trying to selected some data that is deeply nested within the result of the RTK Query api cache.
The issue I'm having is that I'm unsure how to define and use the custom createSelector properly so I can pass in the multiple parameters needed to get the data. I seem to run into the same error of expected 2 arguments but got 3.
From what I understand from the docs, and YouTube/articles, is that you pass in each param which will correspond with an input parameter in the selector. The final thing in the selector is the output where you grab the final data you want.
So I need to pass in 2 params. First one for the query cache signature so I can get the initial data. The second param is to filter the data in the output selector.
slice.ts
export const selectOnboardingCategoryFormInfoResult = (state: RootState, { category_id, project_id }: { category_id: number; project_id?: number }) => (!!project_id ? wordprestoApi.endpoints.getOnboardingFormInfo.select({ category_id, project_id })(state).data : undefined);
export const selectAnswer = (answer: OnboardingAnswersResponse) => answer;
export const selectApprovalByAnswer = createSelector([selectOnboardingCategoryFormInfoResult, (cats, answer: OnboardingAnswersResponse) => selectAnswer(answer)], (queryCategories, answer) => {
if (!queryCategories) return [];
let foundApproval: OnboardingApproval | undefined = undefined;
if (!queryCategories) return foundApproval;
for (const category of queryCategories) {
const questionResult = category.OnboardingQuestions.find(question => question.OnboardingAnswers.length > 0 && question.OnboardingAnswers[0].id === answer?.id);
if (questionResult) {
foundApproval = questionResult.OnboardingAnswers[0].OnboardingAnswerApprovals[0];
break;
}
}
return foundApproval;
});
usage
const storedApproval = useSelector(state => selectApprovalByAnswer(state, { project_id: answer?.project_id, category_id }, answer));
I'm sure I'm missing a key piece of understanding here. Any help would be fantastic :)
Related
I've been following along the REDUX essentials guide and I'm at part 8, combining RTK Query with the createEntityAdapter. I'm using the guide to implement it in a personal project where my getUni endpoint has an argument named country, as you can see from the code snippet below.
I'm wondering is there anyway to access the country argument value from the state in universityAdaptor.getSelector(state => ) at the bottom of the snippet, as the query key name keeps changing.
import {
createEntityAdapter,
createSelector,
nanoid
} from "#reduxjs/toolkit";
import {
apiSlice
} from "../api/apiSlice";
const universityAdapter = createEntityAdapter({})
const initialState = universityAdapter.getInitialState();
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUni: builder.query({
query: country => ({
url: `http://universities.hipolabs.com/search?country=${country}`,
}),
transformResponse: responseData => {
let resConvert = responseData.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(each => {
return { ...each,
id: nanoid()
}
});
return universityAdapter.setAll(initialState, resConvert)
}
})
})
});
export const {
useGetUniQuery
} = extendedApiSlice;
export const {
selectAll: getAllUniversity
} = universityAdapter.getSelectors(state => {
return Object.keys({ ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }).length === 0
? initialState : { ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }
})
UPDATE: I got it working with a turnery operator due to the multiple redux Actions created when RTK Query handles fetching. Wondering if this is best practice as I still haven't figured out how to access the country argument.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
return !Object.values(state.api.queries)[0]
? initialState : Object.values(state.api.queries)[0].status !== 'fulfilled'
? initialState : Object.values(state.api.queries)[0].data
})
I wrote that "Essentials" tutorial :)
I'm actually a bit confused what your question is - can you clarify what specifically you're trying to do?
That said, I'll try to offer some hopefully relevant info.
First, you don't need to manually call someEndpoint.select() most of the time - instead, call const { data } = useGetThingQuery("someArg"), and RTKQ will fetch and return it. You only need to call someEndpoint.select() if you're manually constructing a selector for use elsewhere.
Second, if you are manually trying to construct a selector, keep in mind that the point of someEndpoint.select() is to construct "a selector that gives you back the entire cache entry for that cache key". What you usually want from that cache entry is just the received value, which is stored as cacheEntry.data, and in this case that will contain the normalized { ids : [], entities: {} } lookup table you returned from transformResponse().
Notionally, you might be able to do something like this:
const selectNormalizedPokemonData = someApi.endpoints.getAllPokemon.select();
// These selectors expect the entity state as an arg,
// not the entire Redux root state:
// https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
const localizedPokemonSelectors = pokemonAdapter.getSelectors();
const selectPokemonEntryById = createSelector(
selectNormalizedPokemonData ,
(state, pokemonId) => pokemonId,
(pokemonData, pokemonId) => {
return localizedPokemonSelectors.selectById(pokemonData, pokemonId);
}
)
Some more info that may help see what's happening with the code in the Essentials tutorial, background - getLists endpoint takes 1 parameter, select in the service:
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId) : [];
};
And my selector in the slice:
export const selectAllLists = createSelector(getListsResult, (listsResult) => {
console.log('inside of selectAllLists selector = ', listsResult);
return listsResult.data;
// return useSelector(listsResult) ?? [];
});
Now this console logs listsResult as ƒ memoized() { function! Not something that can have .data property as tutorial suggests. Additionally return useSelector(listsResult) - makes it work, by executing the memoized function.
This is how far I got, but from what I understand, the code in the Essentials tutorial does not work as it is...
However going here https://codesandbox.io/s/distracted-chandrasekhar-r4mcn1?file=/src/features/users/usersSlice.js and adding same console log:
const selectUsersData = createSelector(selectUsersResult, (usersResult) => {
console.log("usersResult", usersResult);
return usersResult.data;
});
Shows it is not returning a memorised function, but an object with data on it instead.
Wonder if the difference happening because I have a parameter on my endpoint...
select returns a memoized curry function. Thus, call it with first with corresponding arg aka tribeId in your case and then with state. This will give you the result object back for corresponding chained selectors.
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId)(state) : [];
};
The intention of the getUni endpoint was to produce an array of university data. To implement the .getSelector function to retrieve that array, I looped over all query values, searching for a getUni query and ensuring they were fulfilled. The bottom turnery operator confirms the getUni endpoint was fired at least once otherwise, it returns the initialState value.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
let newObj = {};
for (const value of Object.values(state.api.queries)) {
if (value?.endpointName === 'getUni' && value?.status === 'fulfilled') {
newObj = value.data;
}
}
return !Object.values(newObj)[0] ? initialState : newObj;
})
I need to use 2 queries in my file and I am writing them like so:
const {loading, data } = useQuery(getCharactersQuery);
const {loading, data} = useQuery(getSingleCharacterQuery);
The problem is, they both have the same "loading" and "data" variables and I don't see anywhere in the docs how can we have different ones. How can I differentiate them?
It's Object destructuring of JS Destructuring assignment. You can choose not to use it here to give different variable names.
const resCharacters = useQuery(getCharactersQuery);
const resSingleCharacter = useQuery(getSingleCharacterQuery);
if (resCharacters.loading || resSingleCharacter.loading) return 'Loading...';
...
Ref: Apollo document of useQuery
This way, by giving them an alias.
const {loading, data } = useQuery(getCharactersQuery);
const {loading: singleCharacterLoading, data: singleCharacterData} = useQuery(getSingleCharacterQuery);
const GET_DATA = gql`
query {
characters {
phone
rating
ratingType
review
city
id
}
singleCharacter {
title
text
}
}
`;
const {loading, data } = useQuery(GET_DATA);
console.log(data) //{characters: [...], singleCharacter: [...]}
Suppose data - is data from a parent query.
Child react-component:
const ShowDetails = ({data}) => {
const { loading, error, data_details } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache -- asks for additional fields that are missing in data.
When (!loading && !error) data_details will have requested fields.
Issue: data_details will have only requested fields.
Question: Is there a way to use parent data with merged-additional-requested fields in ShowDetails and ignore data_details?
In Chrome with help of Apollo devtools I see that apollo-cache has one entry from merged data and data_details.
I do not want to re-fetch all existed entries in data.
Example:
Parent component query:
const bookQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
author
}
}
`
Details query:
const bookEditionsQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
editions {
publisher
year
}
}
}
`
const bookReviewQuery = gql`
query ($bookId: ID!) {
book(id: $bookId) {
id
review {
user
score
date
}
}
}
`
All this queries will populate the same bucket in Apollo cache: book with id.
What is necessary to achieve: in react component BookDetails:
have 1 object with:
data.author
data.editions[0].year
data.review[0].user
Logically - this is one entry in cache.
Thank you for your help.
Almost nothing to save by using already fetched [and passed from parent] data ... only author ... all review and edition must be fetched, no cache usage at all.
... fetching review and editions by book resolver helps apollo cache to keep relation but also requires API to use additional ('book') resolver [level] while it is not required ... review and editions resolvers should be callable directly with book id ... and f.e. can be used by separate <Review /> sub component ... or review and editions called within one request using the same id parameter.
Just use data and dataDetails separately in component - avoid code complications, keep it simply readable:
const ShowDetails = ({data}) => {
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache);
}
if(loading) return "loading...";
return (
<div>
<div>author: {data.author}</div>
{dataDetails.review.map(...
... if you really want to join data
const ShowDetails = ({data}) => {
const [bookData, setBookData] = useState(null);
const { loading, error, data:dataDetails } = useQuery(someQueryAsksAdditionalFileldsForEntryAlreadyPresentInCache, {
onCompleted: (newData) => {
setBookData( {...data, ...newData } );
}
});
if(bookData) return ...
// {bookData.author}
// bookData.review.map(...
I have a component that needs to tap into the React Router query params, and I am using the use-react-router hook package to access them.
Here is what I am wanting to do:
import React from "react;
import useReactRouter from "use-react-router";
const Foo = () => {
const { id } = useReactRouter().match.params;
return (
<Bar id={id}/>
)
}
The issue is that this throws the following error in VS Code, and at compile time:
Property 'id' does not exist on type '{}'.ts(2339)
I have found that if I refactor my code like so:
const id = match.params["id"], I do not get the error, but I feel like this is not the correct approach for some reason. If someone could point me in the right direction, I would appreciate it.
I figured it out. The solution was to include angle brackets between the hook's name and the parenthesis, like so:
const { match } = useRouter<{ id: string }>();
const { id } = useRouter<{ id: string }>();
Or if you prefer nested destructuring:
const { match: { params: id } } = useRouter<{ id: string }>();
You can try to give default value to params
const { id } = useReactRouter().match.params || {id: ""};
It may be possible that params to be null at initial level
The code is insufficient.
However, at first glance,
// right way
const { history, location, match } = useReactRouter()
// in your case
const { match: { params : { id } } } = useReactRouter()
// or
const { match } = useReactRouter()
const { id } = match.params
now, try to console the value first.
Also, please try to pass the props to a functional component from it's container, since it's more logical.
From your comment below, i can only assume you solved it. Also, it's recommended to handle possible undefined values when you use it.
{ id && id }
However, the first step should've been consoling whether it has value in it,
console.log('value xyz', useReactRouter())
Im using React with Meteor. Im passing data to my Event React component with withTracker, which gets and ID from the URL:
export default withTracker(props => {
let eventsSub = Meteor.subscribe('events');
return {
event: Events.find({ _id: props.match.params.event }).fetch(),
};
})(Event);
This is working but I now need to get data from another collection called Groups. The hard bit is that I need to get an ID from the event that I'm already returning.
The code below works when I hardcode 1. However 1 is actually dynamic and needs to come from a field return from the event query.
export default withTracker(props => {
let eventsSub = Meteor.subscribe('events');
let groupsSub = Meteor.subscribe('groups');
return {
event: Events.find({ _id: props.match.params.event }).fetch(),
group: Groups.find({ id: 1 }).fetch(),
};
})(Event);
#Yasser's answer looks like it should work although it will error when event is undefined (for example when the event subscription is still loading).
When you know you're looking for a single document you can use .findone() instead of .find().fetch(). Also when you're searching by _id you can just use that directly as the first parameter. You should also provide withTracker() with the loading state of any subscriptions:
export default withTracker(props => {
const eventsSub = Meteor.subscribe('events');
const groupsSub = Meteor.subscribe('groups');
const loading = !(eventsSub.ready() && groupsSub.ready());
const event = Events.findOne(props.match.params.event);
const group = event ? Groups.findOne(event.id) : undefined;
return {
loading,
event,
group,
};
})(Event);
There's another issue from a performance pov. Your two subscriptions don't have any parameters; they may be returning more documents than you really need causing those subscriptions to load slower. I would pass the event parameter to a publication that would then return an array that includes both the event and the group.
export default withTracker(props => {
const oneEventSub = Meteor.subscribe('oneEventAndMatchingGroup',props.match.params.event);
const loading = !oneEventSub.ready();
const event = Events.findOne(props.match.params.event);
const group = event ? Groups.findOne(event.id) : undefined;
return {
loading,
event,
group,
};
})(Event);
The oneEventAndMatchingGroup publication on the server:
Meteor.publish(`oneEventAndMatchingGroup`,function(id){
check(id,String);
const e = Events.findOne(id);
return [ Events.find(id), Groups.find(e.id) ];
});
Note that a publication has to return a cursor or array of cursors hence the use of .find() here.
It's not very clear which field of the event object should be supplied to Groups.find. But I'll try answering the question.
Try using something like this -
export default withTracker(props => {
let eventsSub = Meteor.subscribe('events');
let groupsSub = Meteor.subscribe('groups');
event = Events.find({ _id: props.match.params.event }).fetch();
return {
event,
group: Groups.find({ id: event['id'] }).fetch(),
};
})(Event);
Note this line -
group: Groups.find({ id: event['id'] }).fetch(),
can be modified to use whichever field you need.
group: Groups.find({ id: event['whichever field'] }).fetch(),