NGRX - combine selectors with props - ngrx

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 })
);

Related

Selector returns empty array from the redux state, even though the array has values

I have the following normalized redux state:
rootReducer: {
blocks: {
"key1": {
id: "key1",
beverages: [], // Array of objects
}
}
}
and I'm trying to select the value of beverages for beverage with the id of "key1" using this selector:
export const getBlockBeverages = (state, blockId) => {
console.log("selector", state.blocks[blockId].beverages);
return state.blocks[blockId].beverages;
};
Whenever I add a new beverage into the beverages array, the selector gets called twice, first time with an empty array, second time with proper values:
Initial state
selector []
selector []
Adding new beverage:
selector []
selector [{/*beverage1*/}]
// Adding another beverage
selector []
selector [{/*beverage1*/}, {/*beverage2*/}]
I'd really appreciate any help/explanation why does the selector get called and beverages value for the block instance is an empty array.
Below is the code for reducers I'm using - I don't see where I could be mutating the original state, I used Immer's produce from the beginning and the problem is still present. Then I tried to use lodash.clonedeep to make sure that I return a new state, but the selector still logs that empty array.
const blockReducer = (state = { id: "", beverages: [] }, action) => {
if (action.type === ADD_BEVERAGE_TO_BLOCK) {
const { beverageId } = action.payload;
const newBeverage = { id: uuid4(), beverageId };
return produce(state, (draft) => {
draft.beverages.push(newBeverage);
});
}
return state;
};
const blocks = (state = {}, action) => {
const key = action.payload.key;
if (key && (state[key] || action.type === CREATE_BLOCK)) {
const instanceState = blockReducer(state[key], action);
return produce(state, (draft: any) => {
draft[key] = instanceState;
});
}
return state;
};
Any ideas why the selector returns empty array instead of array of length 0, 1, 2 etc. as I'm adding new beverages? I'm clueless and will appreciate any help.
The problem was in a different selector that I had been using in a wrong way.
export const makeGetBlockBeveragesLength = () => createSelector(
(state, blockId) => getBlockBeverages(state, blockId),
(blockBeverages) => blockBeverages.length,
);
and instead of mapStateToProps I used createMapStateToProps:
const createMapStateToProps = (state, { blockId }) => () => {
const getBlockBeveragesLength = makeGetBlockBeveragesLength();
return {
length: getBlockBeveragesLength(state, blockId),
};
};
export const Component = connect(createMapStateToProps)(MyComponent);
The empty array logged in one of the logs refers to an older state (the initial state in this case).
I fixed the code to this and it works:
export const getBlockBeveragesLength = createSelector(
(state, blockId) => getBlockBeverages(state, blockId),
(blockBeverages) => blockBeverages.length,
);
const mapStateToProps = (state, { blockId }) => ({
length: getBlockBeveragesLength(state, blockId),
});
export const Component = connect(mapStateToProps)(MyComponent);

Pass React.Context to Nextjs after ComponentDidMount?

I have an issue where I have a simple React.Context that's populated after all the components mount. The problem is that because it happens after mount, nextjs does not see this data on initial render, and so there's noticeable flicker.
Here's the simple component that sets the Context:
export const SetTableOfContents = (props: { item: TableOfContentsItem }) => {
const toc = useContext(TableOfContentsContext);
useEffect(() => {
// Updates the React.Context after the component mount
// (since useEffects run after mount)
toc.setItem(props.item);
}, [props.item, toc]);
return null;
};
Here's the React.Context. It uses React state to store the TOC items.
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const [items, setItems] = useState<TableOfContents["items"]>([]);
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
setItems((items) => items.concat(item));
},
};
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Currently, it is not possible to set the React.Context before mount because React gives a warning---Cannot update state while render.
The only workaround I can think of is to use something other than React.state for the React.Context state---that way the component can update it any time it wants. But then the problem with that approach is that Context Consumers would no longer know that the items changed (because updates live outside the React lifecycle)!
So how to get the initial React.Context into the initial SSR render?
const items = [];
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
items[item.index] = item;
},
};
// this dep never changes.
// when you call this function, values never change
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Here's what I ended up doing:
render the app in getStaticProps using renderToString
use useRef for state in the Context instead of useState
the reason for doing this is because renderToString renders only the initial state. So if you update the Context using useState, it won't capture subsequent renders
update the Context on component initialization for the reason mentioned above
pass the Context an "escape hatch"---a function we can call to get the state calculated on the initial render
Yes, the whole thing seems like a giant hack! :-) I'm not sure if React.Context plays well with SSR :(
export const TableOfContentsProvider = (props: {
initialItems?: TableOfContentsItem[];
setItemsForSSR?: (items: TableOfContentsItem[]) => void;
children?: React.ReactNode;
}) => {
// use useRef for the reasons mentioned above
const items = useRef(props.initialItems || []);
// Client still needs to see updates, so that's what this is for
const [count, setCount] = useState(0);
const { setItemsForSSR } = props;
const setterValue = useMemo(
() => ({
setItem(item: TableOfContentsItem) {
if (!items.current.find((x) => x.id === item.id)) {
items.current.push(item);
items.current.sort((a, b) => a.index - b.index);
setCount((count) => count + 1);
setItemsForSSR?.(items.current);
}
},
}),
[setItemsForSSR]
);
const stateValue = useMemo(() => ({ items: items.current, count }), [count]);
return (
<TableOfContentsSetterContext.Provider value={setterValue}>
<TableOfContentsStateContext.Provider value={stateValue}>
{props.children}
</TableOfContentsStateContext.Provider>
</TableOfContentsSetterContext.Provider>
);
};
interface TableOfContentsSetterWorkerProps {
item: TableOfContentsItem;
setItem: (item: TableOfContentsItem) => void;
}
export class TableOfContentsSetterWorker extends React.Component<
TableOfContentsSetterWorkerProps,
{}
> {
constructor(props: TableOfContentsSetterWorkerProps) {
super(props);
// Need to do this on init otherwise renderToString won't record it
props.setItem(props.item);
}
render() {
return null;
}
}
/**
* Usage: use this as a child component when the parent needs to set the TOC.
*
* Exists so that a component can set the TOC without triggering
* an unnecessary render on itself.
*/
export function TableOfContentsSetter(props: { item: TableOfContentsItem }) {
const { setItem } = useContext(TableOfContentsSetterContext);
return <TableOfContentsSetterWorker item={props.item} setItem={setItem} />;
export const getStaticProps = async () => {
let initialTableOfContents: TableOfContentsItem[] = [];
const getItems = (items: TableOfContentsItem[]) => {
initialTableOfContents = [...items];
};
const app = () => (
<TableOfContentsProvider setItemsForSSR={getItems}>
<AppArticles />
</TableOfContentsProvider>
);
renderToString(app());
return {
props: {
initialTableOfContents,
},
};
};

How to pass parameters to the result Function of a createSelector (reselect)

My redux store has two independent slices A and B,
I need a memorized selector which returns something derived from B, only if A changes.
For example
const getSliceA = (state) => state.A
export const getSliceB = createSelector(
getSliceA,
(a) => { return MyDerive(state.B) }
)
My problem is how to send the state or state.B to the resultFunc.
const compareBySliceA = (prevSate: RootState, newState: RootState) => {
// This is just an example you can compare inner of Slice A
if (newState.SliceA === prevState.SliceB) {
return true
}
return false;
};
const getDerivedSliceB (state: RootState): List<any> =>
state.SliceB.filter(ElementB => ElementB.visible)
const createComparatorSelector = createSelectorCreator(
defaultMemoize,
compareBySliceA,
);
export const myDeepSelector = createComparatorSelector(
(state: RootState) => state,
(state: RootState): List<any> => getDerivedSliceB(state),
);
Both compareBySliceA and getDerivedSliceB needs the common parent, that is in above example State is the parnet of SliceA and SliceB.

State does not change in my reducer when action is dispatched

I am not able to retrieve the state in the reducer
MyComponent looks like this
const MyComponent = ({name, features, onClick}) => {
return (
<div>
Hello! {name}
<Button onClick={() => { onClick(features); }}> Weight</Button>
</div>
);
const mapDispatchToProps = (dispatch: any) => {
return {
onClick: (features) => {
dispatch(weightSort(features));
}
};
};
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: "John Doe",
features: ownProps.features,
};
};
export const FeatureBlock = connect(mapStateToProps, mapDispatchToProps)(MyComponent);
My actions and reducers looks like below:
// Action Creator
export const weightSort = (features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
filter: "DESC",
features,
};
};
// Reducer
export const weightFilter = (state = [], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // Gives me empty state
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
export const FeatureBlock = connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent);
What am I missing here? Any help will be appreciated!
In your reducer, when you console.log(state), it is correct in returning an empty array because you haven't done anything to modify it.
// Reducer
export const weightFilter = (state = [1,2,3], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // This will show [1,2,3] because [1,2,3] is the initial state.
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
My guess is that you want something like this for your reducer:
// Action Creator
export const weightSort = (name, features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
name,
features,
};
};
// Reducer
export const weightFilter = (
state = {
name: '',
features: [],
},
action
) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
return {...state, name: action.name, features: action.features}
default:
return state;
}
};
and then in your mapStateToProps you would map out the attributes like so:
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: state.weightFilter.name,
features: state.weightFilter.features,
};
};
and your button would have a name prop passed into the function like so:
<Button onClick={() => { onClick(name, features); }}> Weight</Button>
If you would like to sort your data, you can do so either in the reducer or inside the container. I prefer to do it in the container and like to use the lodash sortBy function. It works like this:
import { sortBy } from 'lodash' //be sure to npm install lodash if you use this utility
...
...
function mapStateToProps(state) {
return {
name: state.weightFilter.name,
features: sortBy(features, ['nameOfPropertyToSortBy'])
};
}
Here is the lodash documentation on sortBy: https://lodash.com/docs/4.17.4#sortBy
Hope that helps!

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