Selector with dynamic argument not memoized - redux

I'm trying to get Reselect to memoize my selector called with a dynamic argument
My selector:
import { createSelector, defaultMemoize } from 'reselect'
const allStatesSelector = store => store.state.states
export const getAllStates = createSelector(
allStatesSelector,
states => defaultMemoize(night => {
console.log('getAllStates called with night:', night)
const availableStates = night ? nightStates : dayStates
return states.filter(state => availableStates.includes(state.id))
}),
)
I'm calling it like so:
states: getAllStates(store)(props.night)
The selector is called 1800 times instead of just 2 (considering night can only be true or false).
What did I miss?

Related

How to make memoization work with useCallback() and Redux-Toolkit createSelector()?

According to the documentation here:
Creating Unique Selector Instances:
There are many cases where a selector function needs to be reused across multiple components. If the components will all be calling the selector with different arguments, it will break memoization - the selector never sees the same arguments multiple times in a row, and thus can never return a cached value.
The standard approach here is to create a unique instance of a memoized selector in the component, and then use that with useSelector. That allows each component to consistently pass the same arguments to its own selector instance, and that selector can correctly memoize the results.
For function components, this is normally done with useMemo or useCallback:
When I did the following on codesandbox:
import { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, selectSum } from "./reducer";
function Sum({ value }) {
const selectSumMemoized = useCallback(selectSum, []);
const sum = useSelector((state) => selectSumMemoized(state, value));
return (
<div>
sum: {sum} (10+100+{value})
</div>
);
}
export default () => {
const dispatch = useDispatch();
return (
<div>
<Sum value={1} />
<Sum value={2} />
<button onClick={() => dispatch(increment())}>
change state and re-render
</button>
</div>
);
};
as I initiated a re-render by clicking the button repeatedly, the output selector of selectSum logged twice, implying that memoization is not working across multiple components.
What changes do I need to make for it to work correctly?
I figured it out.
Basically, I need to define and use the following function (just add ()=>):
const makeSelectSum = () =>
createSelector([select100, select10, (state, n) => n], (a, b, num) => {
console.log("output selector called: " + num);
return a + b + num;
});
Then:
const selectSumMemo = useMemo(makeSelectSum, []);
const sum = useSelector((state) => selectSumMemo(state, value));
or:
const selectSumMemo = useCallback(makeSelectSum(), []);
const sum = useSelector((state) => selectSumMemo(state, value));
instead of using this one directly:
const selectSum =
createSelector([select100, select10, (state, n) => n], (a, b, num) => {
console.log("output selector called: " + num);
return a + b + num;
});

Remove item from redux form field array by its name and not index

Redux Form has FieldArray field:
https://redux-form.com/6.0.0-rc.3/docs/api/fieldarray.md/
I am trying to delete multiple of items from it but remove() method only works for a single removal perhaps because each time the fields get one item smaller and the index determined by me is bigger than the fields array:
<MultiSelect
placeholder="Delete project group"
onChange={(v) => {
const diff = difference(addedGroups, v)
if (!isEmpty(diff)) {
const groupToDelete = diff[0]
forEach(projectsByGroup[groupToDelete], p => removeElement(addedProjects.indexOf(p)))
deleteGroup(groupToDelete)
}}
options={projectGroupNames}
value={addedGroups}
inline
/>
Where removeElement is fields.remove FieldArray function. How to remove correctly multiple items from FieldArray selectively?
Update:
I have also tried to use change in my reducers like that:
import { change } from 'redux-form'
export const deleteVariantSearchProjectGroup = (projectGroupGuid) => {
return (dispatch, getState) => {
const state = getState()
const projectsInGroup = state.projectsByProjectGroup[projectGroupGuid]
const allProjectFields = getProjectsFamiliesFieldInput(state)
const remainingProjectFields = allProjectFields.filter(projectField => !projectsInGroup.includes(projectField.projectGuid))
change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields)
dispatch({ type: UPDATE_VARIANT_SEARCH_ADDED_GROUPS, newValue: without(getState().variantSearchAddedProjectGroups, projectGroupGuid) })
}
}
I get correctly an array remainingProjectFields but then change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields) does not do anything.
I was not able to actually find a way to remove fields one by one with fields.remove but ultimately I solved it by using a reducer and updating Redux Form state using change method:
import { change } from 'redux-form'
export const deleteVariantSearchProjectGroup = (projectGroupGuid) => {
return (dispatch, getState) => {
const state = getState()
const projectsInGroup = state.projectsByProjectGroup[projectGroupGuid]
const allProjectFields = getProjectsFamiliesFieldInput(state)
const remainingProjectFields = allProjectFields.filter(projectField => !projectsInGroup.includes(projectField.projectGuid))
dispatch(change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields))
dispatch({ type: UPDATE_VARIANT_SEARCH_ADDED_GROUPS, newValue: without(getState().variantSearchAddedProjectGroups, projectGroupGuid) })
}
}
and deleteVariantSearchProjectGroup = deleteGroup in the very first jsx code snippet in the question.

Redux pre process store before mapStateToProps?

I have a redux store with multiple teams.
const store = {
selectedTeamId: 'team1';
teams: {
team1: { ... },
team2: { ... },
team3: { ... },
},
};
At any given time a teamId is set.
Now given that I must select the team using the ID each time I call mapStateToProps(), I feel this is cumbersome.
Instead of doing this all the time:
mapStateToProps({ selectedTeamId, teams }) {
return {
team: teams[selectedTeamId],
}
}
Can I pre-process the store using some middleware instead of repeating this pattern in map state to props?
Approach suggested by Redux docs is to create a selector for currently active team and reuse it across all components
// selector itself is a pure function of state
// usually put in separate file, or in file with reducer
const activeTeamSelector = state => state.teams.teams[state.teams.selectedTeamId]
// in connect
const mapStateToProps = (state) => ({
activeTeam: activeTeamSelector(state),
})
That, of course, if you are using combineReducers and teams reducer is called teams in state. If you aren't, and selectedTeamId and teams are contained right in your store, following will work
const activeTeamSelector = state => state.teams[state.selectedTeamId]
Notice how I only had to change selector for this, and not every mapStateToProps in all the components
read more about Normalizing Store State and Computing Derived Data in Redux docs
Using a middleware for this scenario isn't performant (if I understood your question correctly :) ). I will outline 3 options you can use to achieve this:
Option 1
return both selectedTeamId and teams in mapStateToProps, this will allow you to find the team you need for each selected id:
mapStateToProps({ selectedTeamId, teams }) {
return {
selectedTeamId,
teams
}
}
That way you can access these props in render:
render() {
const { teams, selectedTeamId } = this.props;
return <Team team={teams.find(team => team.id === selectedTeamId)} />
}
Note: <Team /> is just a component I made for demonstration
Option 2
you can use reselect library to avoid recomputing this prop:
import { createSelector } from 'reselect'
const teams = state => state.teams;
const selectedTeamId = state => state.selectedTeamId;
const subtotalSelector = createSelector(
teams,
selectedTeamId,
(teams, selectedTeamId) => items.find(team => team.id === selectedTeamId)
)
Option 3
Create an action that will dispatch 'SELECT_TEAM' with the teamId
export function setSelectedTeam(id) {
return { type: types.SELECT_TEAM, payload: id };
}
Create a reducer for that type and return selectedTeam:
[types.SELECT_TEAM]: (state, payload)=> {
return {
...state,
selectedTeam: state.teams.find(team => team.id === payload.id)
};
},
That way you can have a selector for selectedTeam
export const getSelectedTeam = state => state.selectedTeam;
Hope it helps
I eventually used reselect, with thanks to the recommendation of #jank.
One of things I wanted to do was abstract away the need for selectors to appear in mapStateToProps. In order to do that, I wrapped redux connect. This allows insertion of a denormalizer function before mapStateToProps.
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
const getActiveTeamId = state => state.activeTeamId;
const getAllTeams = state => state.teams;
const teamSelector = createSelector(
getActiveTeamId,
getAllTeams,
(activeTeamId, teams) => teams[activeTeamId],
);
function denormalizer(mapStateToProps) {
return state => {
return mapStateToProps({ team: teamSelector(state) });
};
}
export default function reConnect(mapStateToProps = null, actions = null) {
const denormalizedMapStateToProps = denormalizer(mapStateToProps);
return function callConnect(Component) {
return connect(denormalizedMapStateToProps, actions)(Component);
};
}

NGRX - combine selectors with props

How do I combine reducers, when one of them needs props?
I have following model:
interface Device {
id: string;
data: IDeviceData;
}
and DeviceReducer that looks as follow:
import { EntityState, EntityAdapter, createEntityAdapter } from '#ngrx/entity';
import { Device } from '../model/device';
import { SubnetBrowserApiActions } from 'src/app/explorer/actions';
export interface State extends EntityState<Device> { }
export const adapter: EntityAdapter<Device> = createEntityAdapter<Device>();
export const initialState: State = adapter.getInitialState();
export function reducer(
state = initialState,
action:
| SubnetBrowserApiActions.SubnetBrowserApiActionsUnion
): State {
switch (action.type) {
case SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces: {
return adapter.upsertMany(action.payload.entries, state);
}
default: {
return state;
}
}
}
const {
selectAll,
} = adapter.getSelectors();
export const getAllDevices = selectAll;
In my other reducer, when I want to select devices using an array of ids I use this code:
export const getVisibleDrives = createSelector(
[fromRoot.getAllDevices, getVisibleDrivesSerialNumbers],
(devices, visibleDevicesIds) =>
devices.filter((device) => onlineDevicesIds.includes(device.serialNumber)),
);
This code is very repetitive, so I'd like to add add parametrized selector that will return just these drives, that have id in array that I pass as prop. What I tried to do looks as follows:
Additional selector in DeviceReduced
export const getDrivesWithIds = (ids: string[]) => createSelector(
getAllDevices,
devices => devices.filter(device => ids.includes(device.id))
);
And then combine them in the following way:
export const getVisibleDrives = createSelector(
getVisibleDrivesSerialNumbers,
(ids) => fromRoot.getDrivesWithIds
);
Issue here is that the returned type of this selector is
(ids: string[]) => MemoizedSelector<State, Device[]>
Which makes it impossible for me to do anything useful with this selector. As an example I'd like to filter this list by keyword, and I am not able to use filter method on it:
Example usage
export const getFilteredVisibleDrives = createSelector(
[getVisibleDrives, getKeywordFilterValue],
(visibleDrives, keywordFilter) => {
return visibleDrives
.filter(drive => // in this line there is an error: Property 'filter' does not exist on type '(ids: string[]) => MemoizedSelector<State, Device[]>'
drive.ipAddress.toLowerCase().includes(keywordFilter.toLowerCase()) ||
drive.type.toLowerCase().includes(keywordFilter.toLowerCase()) ||
drive.userText.toLowerCase().includes(keywordFilter.toLowerCase())
);
},
);
See my post NgRx: Parameterized selectors
for more info.
Update: NgRx v13+
Selector with props are deprecated, use selector factories instead:
Selector:
export const getCount = (props: {id: number, multiply:number}) =>
createSelector(
(state) => state.counter[props.id],
(counter) => counter * props.multiply
);
Component:
this.counter2 = this.store.pipe(
select(fromRoot.getCount({ id: 'counter2', multiply: 2 })
);
this.counter4 = this.store.pipe(
select(fromRoot.getCount({ id: 'counter4', multiply: 4 })
);
Deprecated
Selector:
export const getCount = () =>
createSelector(
(state, props) => state.counter[props.id],
(counter, props) => counter * props.multiply
);
Component:
this.counter2 = this.store.pipe(
select(fromRoot.getCount(), { id: 'counter2', multiply: 2 })
);
this.counter4 = this.store.pipe(
select(fromRoot.getCount(), { id: 'counter4', multiply: 4 })
);

Reselect Cannot read property 'get' of undefined

I am using reselect and react redux. I am trying to make a selector for a basic modal implementation.
my selector is
const selectModal = (state) => state.get('modal');
which throws the error of
Cannot read property 'get' of undefined
edit: It has been requested I show how I call select modal, though it should make no difference.
const mapStateToProps = createStructuredSelector({
isVisible: selectModalIsVisible(),
});
const mapDispatchToProps = {
hideModal,
showModal
};
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
I believe this means the modal state container is not being found
Perhaps I am setting up my reducer or store incorrectly. My reducer is
function modalReducer(state = initialState, action) {
switch (action.type) {
case HIDE_MODAL:
return state.set(
'isVisible', false);
case SHOW_MODAL:
return state.set(
'isVisible', true);
default:
return state;
}
}
which is combined with combine reducers into a glob
export default function createReducer(asyncReducers){
return combineReducers({
route: routeReducer,
auth: authReducer,
modal: modalReducer,
...asyncReducers
});
}
and then injected into my store
function configureStore(initialState = {}, history) {
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
]
const store = createStore(
createReducer(),
fromJS(initialState),
compose(...enhancers)
);
store.runSaga = sagaMiddleware.run;
//store.close = () => store.dispatch(END)
store.runSaga(sagas);
store.asyncReducers = {};
return store;
}
var initialState = {}
const store = configureStore(fromJS(initialState), browserHistory);
The error within reselect is at lines 73/74 params = dependencies.map
var selector = function selector(state, props) {
for (var _len4 = arguments.length, args = Array(_len4 > 2 ? _len4 - 2 : 0), _key4 = 2; _key4 < _len4; _key4++) {
args[_key4 - 2] = arguments[_key4];
}
var params = dependencies.map(function (dependency) {
return dependency.apply(undefined, [state, props].concat(args));
});
return memoizedResultFunc.apply(undefined, _toConsumableArray(params));
};
So what am I doing wrong, do I need to do something with immutableJS differently to access the modal, or is my setup for the app incorrect? Thank you in advance for your feedback.
If you're using selectModal like you're using selectModalIsVisible, then your syntax is wrong. I'm pretty sure createStructuredSelector does not understand () => (state) => state.get('modal'). It would only accept (state) => state.get('modal')
Typically, my usages of createStructuredSelector will look like either
const getThing = (state, props) => state.things[props.thingId];
const getModal = state => state.get('modal');
const mapStateToProps = createStructuredSelector({
thing: getThing, // notice no parens
modal: getModal, // notice no parens
})
OR if I need selector factories:
// just pretend this selector was more complicated and needed memoization
const makeGetThing = () => createSelector(
state => state.things,
(state, props) => props.thingId,
(things, thingId) => things[thingId]);
const getModal = state => state.get('modal');
const makeMapStateToProps = () => createStructuredSelector({
thing: makeGetThing(), // yes parens
modal: getModal, // no parens
})

Resources