RTK Query — correct approach for creating endpoint data selectors - redux

I've been playing a bit with RTK Query and I was wondering how to correctly pick data from an endpoint and returning that data filtered (or somehow updated)
I ended up making this to work
export const selectGroupAvailableAssessments = (cacheKey) => {
return rtkQueryApi.endpoints.getGroupAvailableAssessments.select(cacheKey);
};
// returns object with status, endpointName, data, error, isLoading, etc
export const selectGroupAvailableAssessmentsByAssessmentId = (cacheKey, assessmentId) => createSelector(
selectGroupAvailableAssessments(cacheKey),
(availableAssessments) => {
if (!availableAssessments.data) return null;
const { data } = availableAssessments;
return data.find((item) => item.id === assessmentId);
},
);
// returns the selector "data" assessments filtered by id
The in the component
const assessmentById = useSelector(selectGroupAvailableAssessmentsByAssessmentId(cacheKey, assessmentId));
Is this the correct approach for creating selectors in RTK Query? I'm not sure if I'm correct.
Used these links as reference
How to use RTK query selector with an argument?
https://medium.com/nmc-techblog/rtk-query-best-practices-e0296d1679e6
How to call endpoint.select() in RTK query with an argument to retrieve cached data (within another selector)?

You should generally just use the useGetGroupAvailableAssessmentsQuery hook - you can combine that with selectFromResult.
const selectFiltered = createSelector(
result => result.data,
(_, assessmentId) => assessmentId,
(data, assessmentId) => {
return data.find((item) => item.id === assessmentId);
}
)
const result = useGetGroupAvailableAssessmentsQuery(something, {
selectFromResult(result) {
return { ...result, data: selectFiltered(result, assessmentId) }
}
})

Related

React Native state console.log

I am using firebase for my app and the data i read i want to put that in state to use it in different places.
it kinda works but when i want to console.log the state it updates like 30 times a second, am i doing something wrong?
this is my code
const db = firebase.firestore();
const [PBS1Detail, setPBS1Detail] = useState();
db.collection('Track').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
renderTracks(doc)
}
)
});
const renderTracks = (doc) => {
let data = doc.data().data[0].Module;
return setPBS1Detail(data);
}
console.log(PBS1Detail)
i already tried to store it in a variable instead of state but thats not working for me, i can't get the variable from the function global
i am a noob i get it xd
You don't need a return statement when setting state. Also, it looks like you're performing some async function which means that when your component renders for the first time, PBS1Detail will be undefined since the db is a pending Promise.
You could/should put your async functions in useEffect hook:
useEffect(()=> {
db.collection('Track').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
renderTracks(doc)
}
)
});
}, [])
const renderTracks = (doc) => {
let data = doc.data().data[0].Module;
setPBS1Detail(data);
}
Finally, your renderTracks function doesn't seem correct as it appears you're looping over docs and resetting your state each time.
Instead, maybe consider having an array for PBS1Detail
const [PBS1Detail, setPBS1Detail] = useState([]);
Then modify your async call:
useEffect(()=> {
db.collection('Track').get().then((snapshot) => {
let results = []
snapshot.docs.forEach(doc => {
results.push(renderTracks(doc))
}
)
setPBS1Detail(results)
});
}, [])
const renderTracks = (doc) => {
return doc.data().data[0].Module;
}
This way you're only setting state once and thus avoiding unnecessary re-renders and you're saving all of your docs instead of overwriting them.

redux-thunk and in app architecture - want to render only views in views and dispatch GET actions in separate component

I am using react-redux and redux-thunk in my application and there are two things I am trying to do:
I want to be able to share the results of a GET request in two components. I know you can do this by connecting the two components to the store, but I want to make it so if the user lands on X page, then Y page cannot make the same GET request again (these two components are Thumbnail and Carousel). In other words, the GET request should be made once (not 100% sure what best practice is here for redux-thunk), and each component should be able to access the store and render the results in the component (this is easy and I can do)
currently the GET request is the parent of the two children view components, which (I think) doesn't make sense. I only want to render a child view component in the parent view, not a GET request. If unclear it will make more sense if you read my code below
This is parent view (Gallery), which has a child component which dispatches an action to redux (using redux-thunk) that makes an API (FetchImages):
import ...
export default function Gallery() {
return(
<>
<GalleryTabs />
<GalleryText />
<div className="gallery-images-container">
<FetchImages /> ----> this is making an API request and rendering two child view components
</div>
</>
)
}
This is FetchImages, which is dispatching the action (fetchImages) which makes the API call
import ...
function FetchImages({ fetchImages, imageData }) {
useEffect(() => {
fetchImages()
}, [])
return imageData.loading ? (
<h2>Loading</h2>
) : imageData.error ? (
<h2>Something went wrong {imageData.error}</h2>
) : (
<>
<Thumbnail /> -----> these two are views that are rendered if GET request is successful
<Carousel />
</>
)
}
const mapStateToProps = state => {
return {
imageData: state.images
}
}
const mapDispatchToProps = dispatch => {
return {
fetchImages: () => dispatch(fetchImages())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(FetchImages)
I think it makes more sense to have something like this:
import ...
export default function Gallery() {
return(
<>
<GalleryTabs />
<GalleryText />
<div className="gallery-images-container">
<Thumbnail /> -----> Thumbnail should be rendered here but not Carousel ( FetchImages here adds unnecessary complexity )
</div>
</>
)
}
tldr
What are some best practices to follow if two components can dispatch an action which makes a GET request but the dispatch should only be made once per time the user is on the website?
Using redux-thunk, what are some best practices for separating concerns so that children view components are within parent view components and the smarter components which are shared between children view components (such as dispatching actions that make GET requests) are dispatched when the user lands on the page without the views and smarter components being directly together?
I'm a noob so thank you for any help
Your first question: your component container should just dispatch the action that it needs data. How you should store async result in state and later handle result from state is something not covered in this answer but the later example uses a component named List that just dispatches getting a data page, selects the data page and dumps the data page in UI. The tunk action does an early return if the data is already in state.
In production application you probably want to store async api result with loading, error, requested and a bunch of extra info instead of assuming it is there or not there.
Your second question is partly answered by the first answer. Component containers should just dispatch an action indicating they need data and not have to know about the data already being there, already being requested or any of that stuff.
You can group functions that return a promise with the following code:
//resolves a promise later
const later = (time, result) =>
new Promise((resolve) =>
setTimeout(() => resolve(result), time)
);
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => x,//will not remove cache entry after promise resolves
};
};
//temporary memory cache store creator when the promise is done
// the cache key is removed
const createTmpMemCache = () => {
const map = new Map();
const cache = createPermanentMemoryCache(map);
cache.resolved = (key) => map.delete(key);
return cache;
};
//tesgting function that returns a promise
const testPromise = (m) => {
console.log(`test promise was called with ${m}`);
return later(500, m);
};
const permanentCache = createPermanentMemoryCache();
const groupTestPromise = createGroup(permanentCache)(
testPromise,
//note that this causes all calls to the grouped function to
// be stored under the key 'p' no matter what the arguments
// passed are. In the later List example I leave this out
// and calls with different arguments are saved differently
() => 'p'
);
Promise.all([
//this uses a permanent cache where all calls to the function
// are saved under the same key so the testPromise function
// is only called once
groupTestPromise('p1'),//this creates one promise that's used
// in all other calls
groupTestPromise('p2'),
])
.then((result) => {
console.log('first result:', result);
return Promise.all([
//testPromise function is not called again after first calls
// resolve because cache key is not removed after resolving
// these calls just return the same promises that
// groupTestPromise('p1') returned
groupTestPromise('p3'),
groupTestPromise('p4'),
]);
})
.then((result) => console.log('second result', result));
const tmpCache = createTmpMemCache();
const tmpGroupTestPromise = createGroup(tmpCache)(
testPromise,
//all calls to testPromise are saved with the same key
// no matter what arguments are passed
() => 'p'
);
Promise.all([
//this uses a temporary cache where all calls to the function
// are saved under the same key so the testPromise function
// is called twice, the t2 call returns the promise that was
// created with the t1 call because arguments are not used
// to save results
tmpGroupTestPromise('t1'),//called once here
tmpGroupTestPromise('t2'),//not called here using result of t1
])
.then((result) => {
console.log('tmp first result:', result);
return Promise.all([
//called once here with t3 becuase cache key is removed
// when promise resolves
tmpGroupTestPromise('t3'),
tmpGroupTestPromise('t4'),//result of t3 is returned
]);
})
.then((result) =>
console.log('tmp second result', result)
);
const tmpUniqueKeyForArg = createGroup(createTmpMemCache())(
testPromise
//no key function passed, this means cache key is created
// based on passed arguments
);
Promise.all([
//this uses a temporary cache where all calls to the function
// are saved under key based on arguments
tmpUniqueKeyForArg('u1'), //called here
tmpUniqueKeyForArg('u2'), //called here (u2 is different argument)
tmpUniqueKeyForArg('u1'), //not called here (already called with u1)
tmpUniqueKeyForArg('u2'), //not called here (already called with u2)
])
.then((result) => {
console.log('unique first result:', result);
return Promise.all([
tmpUniqueKeyForArg('u1'), //called with u1 tmp cache removes key
// after promise is done
tmpUniqueKeyForArg('u3'), //called with u3
tmpUniqueKeyForArg('u3'), //not called, same argument
]);
})
.then((result) =>
console.log('unique second result', result)
);
Now that we have code to group functions that return promises (function is not called when called again with same argument) we can try to apply this to thunk action creators.
Because a trunk action creator is not (...args)=>result but (...args)=>(dispatch,getState)=>result we can't pass the action creator directly to createGroup I created createGroupedThunkAction that adopts the function to group from (...args)=>(dispatch,getState)=>result to ([args],dispatch,getState)=>result while still returning a function with the right signature: (...args)=>(dispatch,getState)=>result.
Here is the example snippet:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
//resolves a promise later
const later = (time, result) =>
new Promise((resolve) =>
setTimeout(() => resolve(result), time)
);
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(
cache
)((args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) => (dispatch, getState) => {
return group(args, dispatch, getState);
};
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => x,//will not remove cache entry after promise is done
};
};
const initialState = {
data: {},
};
//action types
const MAKE_REQUEST = 'MAKE_REQUEST';
const SET_DATA = 'SET_DATA';
//action creators
const setData = (data, page) => ({
type: SET_DATA,
payload: { data, page },
});
const makeRequest = (page) => ({
type: MAKE_REQUEST,
payload: page,
});
//standard thunk action returning a promise
const getData = (page) => (dispatch, getState) => {
console.log('get data called with page:',page);
if (createSelectDataPage(page)(getState())) {
return; //do nothing if data is there
}
//return a promise before dispatching anything
return Promise.resolve()
.then(
() => dispatch(makeRequest(page)) //only once
)
.then(() =>
later(
500,
[1, 2, 3, 4, 5, 6].slice(
(page - 1) * 3,
(page - 1) * 3 + 3
)
)
)
.then((data) => dispatch(setData(data, page)));
};
//getData thunk action as a grouped function
const groupedGetData = createGroupedThunkAction(
getData,//no getKey function so arguments are used as cache key
createPermanentMemoryCache()
);
const reducer = (state, { type, payload }) => {
console.log('action:', JSON.stringify({ type, payload }));
if (type === SET_DATA) {
const { data, page } = payload;
return {
...state,
data: { ...state.data, [page]: data },
};
}
return state;
};
//selectors
const selectData = (state) => state.data;
const createSelectDataPage = (page) =>
createSelector([selectData], (data) => data[page]);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//improvided thunk middlere
({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
)
)
);
//List is a pure component using React.memo
const List = React.memo(function ListComponent({ page }) {
const selectDataPage = React.useMemo(
() => createSelectDataPage(page),
[page]
);
const data = useSelector(selectDataPage);
const dispatch = useDispatch();
React.useEffect(() => {
if (!data) {
dispatch(groupedGetData(page));
}
}, [data, dispatch, page]);
return (
<div>
<pre>{data}</pre>
</div>
);
});
const App = () => (
<div>
<List page={1} />
<List page={1} />
<List page={2} />
<List page={2} />
</div>
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
In that example there are 4 List components rendered, two for page 1 and two for page 2. All 4 will dispatch groupedGetData(page) but if you check the redux dev tools (or the console) you see MAKE_REQUEST and resulting SET_DATA is only dispatched twice (once for page 1 and once for page 2)
Relevant grouping functions with permanent memory cache is less than 50 lines and can be found here

combineLatest is not getting called

I have the following code
I am new to rjxs programming so wondering what can be the root cause. I am trying to do a certain task and first combine all latest result, then subscribe to assign results. But combineLatest is not working seems like. What can be the cause ?
const downloadUrls$: any = Array.from(event.target.files).map((file: any) => {
const fileName = ...
const path = ...
const fileRef = ....
const task: any = ...
return task.snapshotChanges().pipe(
filter(snap => snap.state === firebase.storage.TaskState.SUCCESS),
switchMap(() => {
return fileRef.getDownloadURL().pipe(
map((url: string) => ({
name: fileName,
filepath: url,
relativePath: path,
}))
);
})
);
});
combineLatest(...downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});
The reason behind that combinelatest doesnt fire is te reference of the subject.
You should change to this version. So removing the ... before the downloadUrls$.
combineLatest(downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});
Combinelatest is for merging two or more observables. Is there any specific reason using combinelatest with one observable? Otherwise you can just change to simpler version like
downloadUrls$.subscribe((urls: any) => {
console.log(hi); // not printing
});
The latest versions of cobineLateste() receive an array as a parameter:
combineLatest(downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});

How can I subscribe and unsubscribe from individual firestore queries?

I am trying to modify an effect I have made into letting me start and stop multiple firestore queries by using two actions. Currently the effect allows me to start and stop a single firestore query by listening for two separate actions in the effect. I simply use a switchMap to switch into an empty observable when there is a stop action. This works just fine.
#Effect()
startStopQuery$ = this.actions$.pipe(
ofType(
ActionTypes.START,
ActionTypes.STOP
),
switchMap(action => {
if (action.type === ActionTypes.STOP) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', 'x');
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
What I actually want to do is to have multiple queries ongoing that I can start and stop with those same two actions, but where it depends on the action payload. When I modified it everytime I performed a new query the last one stops working. I think it is because the switchMap operator switches away from my last query observable. This is the best I have come up with:
#Effect()
startStopQueryById$ = this.actions$.pipe(
ofType(
ActionTypes.START_BY_ID,
ActionTypes.STOP_BY_ID
),
switchMap(action => {
if (action.type === ActionTypes.STOP_BY_ID) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', action.id);
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
As I said, I think the issue is the switchMap operator. But that is also what I depended on to make the "stop" work in the first place. I cant seem to come up with another solution as I am not very well versed in the style yet.
Any help would be greatly appreciated!
I came up with a solution. I make an object that maps ID's to the firestore statechanges observables. On the start action I make the listener and adds it to the object. I make sure that it automatically unsubscribe by piping takeUntil with the corresponding stop action. It returns a merge of all the observables in the object and I silply do as before. I also have a seperate effect triggered by the stop action to remove the observable from the object. It looks like so:
queriesById: {[id: string]: Observable<DocumentChangeAction<Element>[]>} = {};
#Effect()
startQuery$ = this.actions$.pipe(
ofType(ActionTypes.START_BY_ID),
switchMap(action => {
this.queriesByPlay[action.pid] = this.afs.collection<Element>('requests', ref => {
return ref.where('field', '==', action.id);
}).stateChanges().pipe(
takeUntil(
this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
filter(cancelAction => action.id === cancelAction.id),
)
)
);
return merge(
Object.values(this.queriesByPlay)
);
}),
mergeMap(actions => actions),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
Effect({dispatch: false})
stopQuery$ = this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
map(action => delete this.queriesByPlay[action.id]),
);
This seems to work and have no problems except for being convoluted hard to understand.

Assigning Field Value of Firestore Document to Const on Cloud Functions

I would like to assign the value of a field from a document to a constant to use it across several functions.
const stripeAccountId = firestore.doc('orgs/' + subscription.orgId).get()
.then( org => {
return org.data().stripeAccountId
})
The firestore.doc('orgs/' + subscription.orgId).get().then(...) method returns a promise. More info: https://scotch.io/tutorials/javascript-promises-for-dummies
Promises are async and you need to assign the stripeAccountId inside the arrow function specified inside the then.
I don't know where you will use it, but the stripeAccountId will only be filled after the promise is resolved.
const stripeAccountId = null;
firestore.doc('orgs/' + subscription.orgId).get().then(org => {
stripeAccountId = org.data().stripeAccountId;
})
console.log(stripeAccountId); // null
const sufficientTimeInMillisToResolveThePromise = 10000;
setTimeout(() => {
console.log(stripeAccountId); // some-id
}, sufficientTimeInMillisToResolveThePromise);

Resources