Pass a whole object to redux reselect selector, but change it only if one property of the object changes - redux

Started working with Reselect and there's one thing I can't seem to find an answer for.
Say I have a helper fn (getVehicleList) which does some heavy calculations, so I don't want it to re-run too much. I use state selectors to get the properties I need, something like:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
I then have implemented the createSelector:
const getVehicles = createSelector(
[getVehicles,
getVehicle,
getUserId],
(vehicles, vehicle, id) => getVehicleList(
vehicles,
vehicle,
id,
),
);
Now, vehicle returns an object with multiple fields. If any of these fields change, the object changes and so everything is recomputed again. Is there a way to stop this recomputing until the id and only the id of the vehicle changes?
I tried doing a state selector for the id, like
const getVehicle = (state) => state.vehicles.vehicle.id;
But that doesn't work for me, cause I need the whole vehicle object inside my helper fn and not just the id.
Thanks in advance for the help!

You can try the following:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
const selectVhicleId = createSelector(
[getVehicle],
({ id }) => id //return only the id
);
const selectVehicles = createSelector(
[getVehicles, selectVhicleId, getUserId],
(vehicles, vehicleId, id) =>
getVehicleList(vehicles, { id: vehicleId }, id)
);
Here is some information about how I use reselect with React.
Here is an example that re calculate vehicles when vehicle.id changes (or any of the other dependencies). It will not re calculate if other values of vehicle change so the vehicle used getVehicleList gets a stale vehicle passed to it that is only refreshed when vehicle.id changes:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
const createSelectVehicles = (vehicle) =>
createSelector([getVehicles, getUserId], (vehicles, id) =>
getVehicleList(vehicles, vehicle, id)
);
const Component = () => {
//only re calculate vehicle if vehicle.id changes
const vehicle = useSelector(
getVehicle,
(a, b) => a?.id === b?.id
);
//only create the selector when vehicle changes
// vehicle only changes when vehicle.id changes
const selectVehicles = React.useMemo(
() => createSelectVehicles(vehicle),
[vehicle]
);
//vehicles is re calculated when vehicle.id changes
// or when state.vehicles.vehicles changes or
// when state.auth.user.id changes
const vehicles = useSelector(selectVehicles);
};

Related

Firestore query cursor always returning same data

I'm working on a virtualized table that lazily subscribes to a new batch of items in firestore. The issue is that query cursor returns the same batch of data, regardless of the startAt index being different.
const subscribeForItems = (
startIndex: number,
handleItems: (items: FirestoreItem[]) => void,
): (() => void) => {
const firestoreQuery = query(
collection(firestore, RootCollections.Items),
orderBy('price'),
startAt(startIndex),
limit(LIMIT),
);
return onSnapshot(firestoreQuery, (querySnapshot) => {
const items: FirestoreItem[] = [];
querySnapshot.forEach((doc) => {
items.unshift(
FirestoreUtils.extractDocumentData<FirestoreItem>(doc),
);
});
console.log(startIndex)// START INDEX IS DIFFERENT HERE EACH INVOCATION
console.log(items)// ITEMS ARE SAME HERE EACH INVOCATION (AS IF I'M ALWAYS PASSING THE SAME START INDEX)
handleItems(items);
});
};
So the issue was setting the limit. Instead of
const firestoreQuery = query(
collection(firestore, RootCollections.Items),
orderBy('price'),
startAt(startIndex),
limit(LIMIT),
);
it should be
const firestoreQuery = query(
collection(firestore, RootCollections.Items),
orderBy('price'),
startAt(startIndex),
limit(LIMIT + startIndex),
);
I find limit() method to be named counterintuitively, perhaps endAt() would have been a better name, if I need to do the addition myself

How to destructure an Array from an Object in Redux React?

I am using redux and I want to destructure teamMembers - Array from an Object name - teamData which I am getting from REDUX. This is what I have done below. I just want to confirm whether is this the right way to do.
const teamData = useSelector((state) => state.team.teamData);
const { description, heading } = teamData;
const teamMembers = useSelector((state) => state.team.teamData.teamMembers);
If possible, you should not do
const teamData = useSelector((state) => state.team.teamData);
const { description, heading } = teamData;
Your code here is interested in description and heading, but your useSelector selects the whole teamData.
Now, if there is a third property on teamData, let's say winStreak and that winStreak changes, your component will rerender. Because that change to winData.teamStreak caused winData to change. And your useSelector is watching winData for changes.
If you instead do
const description = useSelector((state) => state.team.teamData.description);
const heading = useSelector((state) => state.team.teamData.heading );
your component will only rerender, when description or heading change. Because your useSelector now watches those two properties for changes. It does not care if teamData changes, it only cares if teamData.descriptionorteamData.heading` change.
So, in your case you should do
const description = useSelector((state) => state.team.teamData.description);
const heading = useSelector((state) => state.team.teamData.heading );
const teamMembers = useSelector((state) => state.team.teamData.teamMembers );
and not use any destructuring here.
If you have to destructure the result of a useSelector call, that likely means that your useSelector call is also observing data you are not interested in in the first place.

NGRX selectors: factory selector within another selector without prop in createSelector method

Using the factory selector pattern const selectA = (id: number) => createSelector(...) I have an instance where I want to reuse this selector within another selector (that iterates through an array of IDs) but I don't know the value to pass into the factor selector when calling createSelector.
So I have a selector that I use whenever I want to get a slice of the state for component A.
const selectA = (id: number) =>
createSelector(
selector1.selectEntityMap,
selector2.selectEntityMap,
selector3ById(id),
(
thing1,
thing2,
thing3
) => {
return ...
});
Now I want to get a list of component A for each item in an array.
const selectListOfA = (ids: number[]) =>
createSelector(
selectA,
(selectorA) => {
return ids.map((id) => selectorA(id));
});
The problem is selectA, which is now a factory selector, expects a parameter, but I don't know it when calling createSelector.
I can get the code to compile by creating another factory onto of the factory
const selectAFactory = () => selectA;
And then reference the new factory in the createSelector
const selectListOfA = (ids: number[]) =>
createSelector(
selectAFactory, <<< here
(selectorA) => {
return ids.map((id) => selectorA(id));
});
But of course, what's now happening is the selector is returning a list of MemoizedSelector[].
This pattern doesn't seem like it should be this complicated, are people not reusing their selectors in this way, what am I missing?
The function returned by selectA is a standard function, ie nothing magical about it, as explained well in this blog post: https://dev.to/zackderose/ngrx-fun-with-createselectorfactory-hng
This means selectListOfA can simply call the function returned from selectA for each id and an array of the state slices for component A will be returned:
export const selectListOfA = (ids: number[]) =>
createSelector(
(state) => state,
(state) => ids.map((id) => selectA(id)(state))
);
This works as expected but since the projector function will be executed every time anything in the store changes (recreating the selector for every id) this solution will have major performance issues.
We could just as well simplify the code to this with equally poor performance:
const selectListOfA = (ids: number[]) =>
(state) => ids.map(
(id: number) => selectA(id)(state)
);
If we instead supply an array of selectors as input to the createSelector call then Ngrx will be able to correctly determine when it has to reevaluate the selectA selectors:
const selectListOfA = (ids: number[]) =>
createSelector(
ids.map((id) => selectA(id)), // This results in an array of selectors
(...resultArr) => resultArr
);
However, Typescript will complain since the createSelector method does not have a matching overload declared for an array of variable length so we need to loosen up the input type of the array (to any) as well as specify the return type of selectListOfA.
The answer to the question is thus:
const selectListOfA = (ids: number[]) =>
(createSelector(
ids.map((id) => selectA(id)) as any,
(...resultArr) => resultArr
) as (state) => string[]);

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

can't unsubscribe on my ngrx selector call

I have the following code
selectProduct(id){
const sub = this.store$.select(ProductStoreSelector.selectProductByID(id)).subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
sub.unsubscribe();
});
}
Basically, I would like to get my list of product, and get one by ID, then change my store state so that the selectedProduct become the one I just selected
export const featureAdapter: EntityAdapter<IProduct> = createEntityAdapter<IProduct>({
selectId: model => model.id,
});
export const selectAllProducts: (state: object) => Array<IProduct> = featureAdapter.getSelectors(selectProductsState).selectAll;
export const selectProductByID = (id: string) => createSelector(
selectAllProducts,
(products) => products.find((product) => product.id === id)
);
and my store is an entityState of products with one selected
export interface State extends EntityState<IProduct> {
selectedProduct: IProduct;
}
but the problem is,
althougt I do get my productId back, I can't unsubscribe to sub.unsubscribe() because it is undefined.
You can use either take(1) to listen for values only one time. Otherwise try to unsubscribe like below:
selectProduct(id){
this.store$.select(ProductStoreSelector.selectProductByID(id)).subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
}).unsubscribe();
}
using take(1):
selectProduct(id){
this.store$.select(ProductStoreSelector.selectProductByID(id))
.take(1)
.subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
});
}

Resources