How to extract selectors from adapter from queries with arguments - reactjs

I'm trying to extract selectors from queries in my apiSlice as said in this documentation: https://redux.js.org/tutorials/essentials/part-8-rtk-query-advanced
The documentation put this example:
const usersAdapter = createEntityAdapter()
const initialState = usersAdapter.getInitialState()
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUsers: builder.query({
query: () => '/users',
transformResponse: responseData => {
return usersAdapter.setAll(initialState, responseData)
}
})
})
})
export const { useGetUsersQuery } = extendedApiSlice
// Calling `someEndpoint.select(someArg)` generates a new selector that will return
// the query result object for a query with those parameters.
// To generate a selector for a specific query argument, call `select(theQueryArg)`.
**// In this case, the users query has no params, so we don't pass anything to select()**
export const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()
const selectUsersData = createSelector(
selectUsersResult,
usersResult => usersResult.data
)
export const { selectAll: selectAllUsers, selectById: selectUserById } =
usersAdapter.getSelectors(state => selectUsersData(state) ?? initialState)
Now, i need to have selectById selector (that by default takes userid as second parameter).
I can't manage to have a working selectById selector when my query looks like this
endpoints: builder => ({
getUsers: builder.query({
query: (applicationId) => `/application/${applicationId}/users`,
transformResponse: responseData => {
return usersAdapter.setAll(initialState, responseData)
}
})
})
How do I extract selectById selector from adapter and how i use it in a component with useSelector when I have this kind of query with arguments?
Thanks anyone that will help me

I feel like you mismatch concepts a bit, so it leads to confusion.
First of all, let's clarify, that's selectors you a using- it's not a selector to your state in usersAdapter, but to RTK-Q's own state.
By handling a response in transformResponse you are just copying the data from it to your's, usersAdapter's state.
So, considering that, you should be specific about which state you are going to select from. If you want it from the "final destination", i.e. from the adaptor's state, it's should be done via:
const usersSelectors = **usersAdapter**.getSelectors(
(state) => state.users // or something like that
)
Otherwise, using the selectors from apiSlices like extendedApiSlice from your example - you are fetching the data from RTK-Q's cached state, which may not contain some old data after the cache invalidation. If it's still your goal, the limitation is that RTK-Q's store isn't a normalized store you may expect, with ids and values, but rather the key-value pairs, where keys are your requests, and the values - last results (users arrays in your case). So, if you have no API endpoint defined for selecting a particular user by ID, you won't be able to select it from RTK-Q's state directly. But you may select the cached users by applicationId, and find your user by id in the result array. I bet it's not what you actually want, so most probably you need just to prepare selectors for your own store, as I've mentioned above.

Related

RTK Query how to update query state

I have a route where i fetch all categories
getAllCategoriesAdmin: builder.query({
query: () => 'categories/allAdmin',
providesTags: ['CategoriesAdmin']
})
Then I get categories data from data refactored part of useGetAllCategoriesAdminQuery
const { isLoading, isError, data = {} } = useGetAllCategoriesAdminQuery();
Now I want to implement search field where I want to update that data part with new data in other words when I create new route
getAllCategoriesAdminSearch: builder.query({
query: () => 'categories/allAdminSearch',
providesTags: ['CategoriesAdmin'],
})
Is there any way that I can reupdate data part from useGetAllCategoriesAdminQuery so I dont have to make another data from useGetAllCategoriesAdminSearchQuery
const { data = {} } = useGetAllCategoriesAdminSearchQuery();
I don't get your question completely but I'm assuming you have a query API and you need the search query for this API. I hope this helps you.
there are many ways to re-fetch the data:
1:
I think your API should support some query params for searches like this:
getAllCategoriesAdmin: builder.query({
query: (searchInput) => `categories/allAdmin/?search=${searchInput}`,
providesTags: ['CategoriesAdmin']
})
RTK query creates a cache key base on query URL and params so if you change the search input it will automatically create a new request and fetch the data.
2:
another way is using invalidateTags for example if you have a mutaion query you can invalidate the tags in this case CategoriesAdmin and that causes the RTK clear the cache for corresponding query and refech the data.
3:
refetch function. every useHookQuery in RTK has a re-fetch function that you can assign it to your search event for re-fetching the new data.
const { refetch, isLoading, isError, data = {} } = useGetAllCategoriesAdminQuery();

Graphql - Apollo Client/ React - Cache - fetchMore doesn't update the data state of the query

I try to implement cached pagination, in my react app using apollo client.
my query has filter argument, which should be the only argument that create a new key in the cache object.
for some reason, when fetchMore occurs with filter specified, the new data doesn't cause a re-render in the component.
I logged the existing and incoming argument in the merge function, and it seems that for each fetchMore that had filter, new data did arrive. so, i don't understand why the component didn't re-render.
to make things worst: calling fetchMore several times with or without filter send http request and merging the incoming data with the existing data. which i'd expect wouldn't happen as the client should see that it already has a key in the cache for that query with that key argument.
the following is the query:
query Shells($first: Int = 5, $offset: Int = 0, $filter: ShellFilter) {
shells(
orderBy: [STATUS_ASC, EXECUTION_FROM_DESC]
first: $first
offset: $offset
filter: $filter
) {
nodes {
...ShellData
}
totalCount
}
}
the apolloClient config is like this:
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
shells: {
keyArgs: ['filter'],
merge: (existing, incoming) => {
console.log('existing:', existing, 'incoming:', incoming);
return mergeObjectsAndNestedArrays<ShellsConnection>(
existing,
incoming,
'nodes',
);
},
},
},
},
},
})
and the component that displays it:
const ControlCenter = () => {
const { showModal } = useModalContext();
const [page, setPage] = useState(1);
const { data, loading, fetchMore } = useShellsQuery();
const [query, setQuery] = useURLQuery();
const onCounterpartiesChange = async (counterparties) => {
await fetchMore({
variables: {
filter: { shellCounterParties: { some: { orgId: { in: '20584' } } } },
},
});
setQuery({ counterparties });
};
const shells = data?.shells?.nodes;
console.log('hello from shells:', shells);
these are the logs:
EDIT 1 - docs reference
Following the docs: https://www.apollographql.com/docs/react/pagination/key-args/#setting-keyargs
any argument can be used as the keyArgs: limit, offset and filter.
In the documentation examples, the arg used as the key is a primitive value, but in your case, the filter arg is an object. This could be causing apollo to see all results as the same cached version. If your data depend only on the orgID I think you could try the nested array notation to set that field as the key.
keyArgs: ["filter", ["shellCounterParties", ["some", ["orgId", ["in"]]]]]
or the custom function
keyArgs: (args, context) => args.filter.shellCounterParties.some.orgId.in
If you really need to cache according to the whole filter object, I guess the simplest way would be stringifying it
keyArgs: (args, context) => JSON.stringify(args.filter)
But to be sure how apollo is caching the data, I highly recommend you to try the apollo devtools
related: https://github.com/apollographql/apollo-client/issues/7314
I think the problem lies where you have defined typePolicies in your code with keyArgs: ['filter'].
Please check official docs:
https://www.apollographql.com/docs/react/caching/cache-configuration/#customizing-cache-ids
You can customize how the InMemoryCache generates cache IDs for individual types in your schema
This is helpful especially if a type uses a field (or fields!) besides id or _id as its unique identifier.
Based on this, you have defined filter as a unique identifier even though that is a variable which is used for filtration purpose. It is not a field to customize the cache but a variable.
Note that these keyFields strings always refer to the actual field names as defined in your schema, meaning the ID computation is not sensitive to field aliases.
My suggestion first of all would be to modify the configuration that you have set up and see if it helps?
Instead of fetchMore use refetch inside useEffect and pass there new variables
function photo({ id }) {
const { data, refetch } = useQuery(GET_PHOTO, {
variables: { id },
});
useEffect(() => {
refetch({ id })
}, [id])
}

Lookup multiple Firebase entries using Redux action

I have this Redux action, which looks up a user and returns all the items they have:
export const itemsFetch = () => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items`)
.on('value', snapshot => {
dispatch({ type: ITEMS_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
Works great, and each item returned has a unique key associated with it.
I want to modify this action to look up specific items, which I've done. That works fine too:
export const itemLookup = uid => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${uid}`)
.on('value', snapshot => {
dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: snapshot.val() });
});
};
};
This also works fine, but I can only use this to lookup a single item.
I want to loop over an array of item ids, and lookup details for each. Doing this from a component, and using mapStateToProps, causes the component to rerender each time, losing the previous lookup in the process.
Is it best to loop over the ids I have at a component level, and make multiple calls. Or should I pass the array to the action, and somehow loop over them within the action?
Thanks
I feel like I'm doing something dumb, or misunderstanding Redux completely.
In my opinion, this is one of the few limitations that firebase has (along side with queries) that sometimes make me want to grow hair again and lose it (I am bald).
I am more experienced with Firestore although I have used Database, but I think you are correct that you can only request one item in Firebase. What I would do to solve this, is to create a new action that receives an array of IDs and then executes and array of promises that will query each doc.
Something like (pseudo code, and you might need to wrap your firebase call into a promise):
let promises = [];
arrayIds.forEach(id => {
promises.push(firebase.database().ref.get(id))
})
return Promise.all(promises).then(dispatch(results))
Now, if you find that the amount of results are usually not a lot, it is totally fine (and usually the way Firebase requires you to) to complete the data filtering in the client.
Using the response from sfratini, I managed to work it out. Here's the action:
export const itemLookup = oid => {
const { currentUser } = firebase.auth();
const items = []
return dispatch => {
new Promise(function (res, rej) {
oid.forEach(id => {
firebase.database().ref(`users/${currentUser.uid}/items/${id}`).on('value', snapshot => {
items.push(snapshot.val())
})
})
}).then(dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: items }))
}
I used the same reducer, and now the items array makes it down to component level. Perfect!

Redux react root reducer slowing down

I‘m building an application using react and redux. One functional feature is to filter results based on the state.
So in each class Component I had something like this:
filteredResults = this.filterResults(this.props.value1, this.props.value2,...)
Now I thought that there should be a performance gain if I just add filteredResults to the redux state and introduce a RootReducer. Actually, that slowed down the application. Any Idea why that happened? For me, it is a bit counterintuitive since filteredResults is calculated right now many times.
PS: This is how my RootReducer looked like:
import {_getFilteredResults} from "../components/utils";
const createFilterRootReducer = reducer => (state, action) => {
let reduced_state = reducer(state, action);
let filteredResults = _getFilteredResults(reduced_state.value1, reduced_state.value2, reduced_state.value3, reduced_state.results);
return {...reduced_state, filteredResults:filteredResults}
};
export default createFilterRootReducer;
That was applied on myRootReducer
const rootReducer = combineReducers({
searching: ReducerSearching,
roomOption: ReducerZimmer,
maxPrice: ReducerMaxPrice,
minPrice: ReducerMinPrice,
internalMaxPrice: ReducerInternalMaxPrice,
Address: ReducerAddress,
Results: ReducerResults,
activePage: ReducerActivePage,
favoriteResults:ReducerLikedItems,
resultSort: ReducerSort,
acceptedCookie: ReducerCookies,
headerHeight: ReducerHeaderHeight,
mapView: ReducerMapView,
rehydrate: ReducerRehydrate
});
export default createFilterRootReducer(rootReducer);
One more thing: I was also using 'redux-persist' to persist the state!
Well, it would be helpful if you could provide some additional infos and the code you are using.
I believe there are at least 3 ways to do what you want to achieve.
1. Use createSelector from reselect
The easiest way in my opinion is having a selector to calculate the filtered results based on the actual state of your whole results and the filtering values.
import { createSelector } from 'reselect';
const getResults = state => state.results; // e.g. array of strings
const getFilter = state => state.filter; // e.g. an input value
export const filteredResults = createSelector( getResults, getFilter, ( results, filter ) => {
return results.filter( item => item.indexOf( filter ) != -1 );
});
Now you can provide filteredResults via connect and mapStateToProps to your components without worrying of filtering your results every time.
Note that createSelector is smart enough to perform an equality check on its parameters in order to recompute the function only when any of its parameter changes.
Pros
You don't need to store the filtered results in the state tree
Cons
Since the filtered results are not stored in the tree, you will need to import this selector everywhere you want to access this data.
You won't be able to access it in a direct way e.g. state.xyz.filteredResults
2. Store the filtered results in the tree when storing anything that would change them.
You can calculate and store the filteredResults in the same tree branch where ( and when ) you store the results and the input value.
This is possible only if your original results are store in the same location.
const initialState = {
results: [],
filteredResults: [],
filter: ''
}
const filterResults = ( results, filter ) => results.filter( item => item.indexOf( filter ) )
const myReducer = handleActions({
[SET_RESULTS]: (state, action) => ({
...state,
results: action.payload,
filteredResults: filterResults( action.payload, state.filter )
}),
[SET_FILTER]: (state, action) => ({
...state,
filter: action.payload,
filteredResults: filterResults( state.results, action.payload )
})
}, initialState)
Pros
You will be able to manually access your filteredResults since they are stored in the tree.
Cons
You will need to make sure every action changing anything realted the the filtered results will update them.
You will be storing extra data in the store ( results + filtered results ).
3. Use Redux-Thunk and store the filtered results
This is a different approach that would work.
Basically you can store the filtered results in a different part of the tree when you input, but in order to do so, you will need Redux Thunk middleware
export const onChangeInputValue = ( value ) => {
return (dispatch, getState) => {
const results = getResults( getState() )
const filteredResults = results.filter( item => item.indexOf( value ) )
dispatch( setInputValue( value ) )
dispatch( setFilteredResults( filteredResults ) )
}
}
Pros
you can store the filtered results in a different part of the tree to where the results or the input value are stored.
Cons
you need an extra action for storing the filtered results.
you are dispatching 2 actions separately that might cause an extra rendereing.
Final consideration
You can choose what is the most suitable and viable way depending on your needs.
I personally like to use the selectors (1) and recompute data since that allows me to separate the logic from the actions and reducers.

Apollo can't access queryVariables in update: after a mutation

I am trying to use update: to update a query after performing a mutation. The problem is that the query in the store has several different variables applied and I would like to update the query and return it with the same variables.
I found in the docs that updateQueries has an option to include queryVariables which are the last set of variables that the query was executed with.
I have not found anything that describes how to retrieve the queryVariables or something like it from inside of update.
Inside of update: I can use
lastQuery = Object.keys(store.data.ROOT_QUERY).slice(-1)[0]
which will return a result like "userRecipes({"first":20,"minTime":0,"maxTime":500,"filterType":"Explore","searchTerm":""})"
The hacky way that I am doing this now is to parse that string to pull out the variables so I can finally use readQuery like so:
const lastQuery = Object.keys(store.data.ROOT_QUERY).slice(-1)[0]
const searchPosition = lastQuery.search("searchTerm")
const searchTerm = lastQuery.slice((searchPosition + 13),-3)
// also parsing the lastQuery string for filterType, minTime, maxTime
const data = store.readQuery({
query: QUERY_USER_RECIPES,
variables: {
filterType: filterType,
searchTerm: searchTerm,
minTime: minTime,
maxTime: maxTime,
}
});
This can't be the best way to do this. Is there a simpler way to access variables inside of update?
It seems like there should be a way to read the existing query and variables that are in the store without passing variables with readQuery.
Thanks for taking a look at this issue!
Version
apollo-client#1.4.0
react-apollo#1.4.2
For apollo 2, but should be the same in 1.x
In the docs, you see that you can also pass variables to readQuery.
Here is an example where a user can book an event clicking a BookEvent component, if the mutation succeeds, it is reflected automatically in the upper component EventDetail.
In the component that tiggers the mutation (BookEvent), I pass store and eventId to a function declared in the upper component (EventDetail) and passed through props of the child component:
const onClick = () => createEventTicketMutation({
variables: { eventId: event.id },
update: (store, { data: { createEventTicket } }) => {
updateStoreAfterBooking(store, event.id)
},
})
Here is the function that performs the cache update in the upper component:
const updateCacheAfterBooking = (store, eventId) => {
const data = store.readQuery({
query: EVENT_DETAIL_QUERY,
variables: { id: eventId },
})
data.eventDetail.bookings += 1
store.writeQuery({
query: EVENT_DETAIL_QUERY,
variables: { id: eventId },
data,
})
}
It is passed like so <BookEvent updateStoreAfterBooking={updateCacheAfterBooking} ... />.
Don't forget to pass also the needed variables to writeQuery.

Resources