NgRx Select Errors When Attempting Access on Nested Properties - javascript

I'm getting TypeErrors when using NgRx select functions when accessing nested properties.
I have my root store configured in app.module.ts like this:
StoreModule.forRoot({ app: appReducer }),
where app reducer is just a standard reducer. It sets the state correctly; I can see that in the redux dev tools. The selectors for some nested properties that are erroring are:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state.dropdowns,
);
When I subscribe to these selectors in app.compontent.ts like this
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
app.component.ts:31 ERROR TypeError: Cannot read property 'dropdowns' of null
at app.selectors.ts:18
When I add logging to the selectors higher up the chain, I can see that the only elements logged are the initialState values, which are set to null. I don't think this selector function should fire until the value changes from its initial value. But since it doesn't, its unsurprising that I'm getting this error, since it is trying to access a property on null. Is it a necessity that initialState contain the full tree of all potential future nested properties in order not to break my selectors?
How can I prevent this selector firing when its value is unchanged?
Also, Is the StoreModule.forRoot configured correctly? It is somewhat puzzling to me that creating a "root" store, creates the app key in my redux store parallel to my modules' stores, ie, the module stores are not underneath app.
Edit:
Adding general structure of app.reducer.ts. I use immer to shorten boilerplate necessary for updating nested properties, however I have tried this reducer also as the more traditional kind with spread operator all over the place and it works identically.
import produce from 'immer';
export const appReducer = produce(
(
draftState: rootStateModels.IAppState = initialState,
action: AppActions,
) => {
switch (action.type) {
case AppActionTypes.LoadAppConfig: {
draftState.appConfig.meta.isLoading = true;
break;
}
/* more cases updating the properties accessed in problematic selectors */
default: {
return draftState; // I think this default block is unnecessary based on immer documentation
}
}
}
Edit: Add initialState:
const initialState: rootStateModels.IAppState = {
user: null,
appConfig: {
meta: {isError: false, isLoading: false, isSuccess: false},
data: {
controls: {
dropdowns: null,
}
},
},
};

Because you updated your question the answer is https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged
it allows to emit values only when they have been changed.
store.pipe(
map(state => state.feature.something),
distinctUntilChanged(),
)
requires state.feautre.something to have been changed.
The right way would be to use createSelector function that returns memorized selectors that works in the same way as distinctUntilChanged.

You can use filter operator to make sure it emits values only for valid values, and after that you can use pluck operator to emit value of respective nested property.
store.pipe(
filter(value => state.feature.something),
pluck('feature', 'something'),
)

The dispatch method is async.
So:
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
Here the subscription runs faster than the dispatch so the select returns with null value from your initial state. Simply check this in the selector or add initial state. EX:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state ? state.dropdown : null,
);

Ok, I took a look again in code and updated my answer.
Can you try below given sample.
this.store
.pipe(
// Here `isStarted` will be boolean value which will enable and disable selector.
//This can be derived from initial state, if null it wont go to next selector
switchMap(data => {
if (isStarted) {
return never();
} else {
return of(data);
}
}),
switchMap(data => select(appSelectors.getConfigDropdowns))
)
.subscribe(data => {
console.log("OnInit Dropdowns Data: ", data);
});

Related

Attempted to assign to readonly property

first of all i get my redux array then in my_function copy that into new variable like below :
let transactions_list = useSelector(state => state.transactions_list.value);
let new_transactions_list = [...transactions_list];
when i want to change my new_transactions_list very deeply i got the error
const my_function = () => {
let new_transactions_list = [...transactions_list];
new_transactions_list[yearIndex].data_yearly[monthIndex].data_monthly.push(new_obj);
}
but when i define an array in class(without redux), it's work
Even if you are using the spreading [...transactions_list], you are still only copying the first level of the array, which means that the object below that array is still the same one that redux uses.
You have 2 options:
This is how redux recommends you to update nested object link
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
Or you can use something like immer, which will allow you to update your object even with immutable like this
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
Either way, you will have to update your redux state afterward since this change will only be local in your code and not the global redux.

My mapStateToProps is not called after adding custom object array to redux state

I am trying for few hours but can't figure out why my state is not called after adding an array of custom object.
// In my component...
const myRemoteArray = getRemoteArray() // Is working
props.addAdItems(myRemoteArray) // Calls **1 via component.props
/// ...
const mapDispatchToProps = (dispatch) => {
return {
addAdItems: (items) => { // **1
// Items contains my array of objects
dispatch(addAdItems(items)) // Calls **2
},
}
}
// My action
export const addAdItems = (items) => { // **2
// Items contains my array of objects
return { // Calls **3
type: AD_ITEMS,
adItems: items,
}
}
const productsReducer = (state = initialState, action) => {
switch (action.type) { // **3
case AD_ITEMS:
// Is working!
// action.adItems contains my array!
const _state = {
...state,
adItems: action.adItems, // Here is the issue, I am not sure how to add my NEW array to existing state and update it.
// Like that: ??? "adItems: ...action.adItems" or adItems: [action.adItems]
}
// The new state contains my Array!!!
return _state
default:
return state
}
}
// In my component... !!!!
// THIS IS NOT CALLED or it is called with empty array from initialState!!!
const mapStateToProps = (state) => {
return {
updatedItem: state.changedItem,
adItems: state.adItems,
}
}
It seems to me that Redux is having a problem with my array containing the following data. Has Redux issues with my class methods?
class Ad {
constructor(
id,
isPublished
) {
this.id = id
this.isPublished = isPublished
}
someMessage = () => { return "Help me!" }
needHelp = () => { return true }
}
My Redux is working already with other calls, data, and objects, which means my createStore and all other stuff is correct.
PS: I don't have multiple stores.
UPDATE
Now my mapDispatchToProps is called with current array but is not persisting.
UPDATE 2
If I save my file and force to refresh the App, the props.adItems contains my loaded array, but if I want to access props.adItems at runtime (e.g. on FlatList refresh) it is empty array again!
Why?
Should I store my array in a useState property after it has changes via useEffect?
You were pretty close in the comments you added in the reducer, but neither of them were 100% accurate.
For Redux to notice that your array has changed, you need the property adItems of your new state to return an entirely new array. You can do it like this:
adItems: [...action.adItems]
With this code you'll be creating a new array, and then adding a copy of the items of the old one into it.
The reason why your current implementation (adItems: action.adItems) is not working is that action.adItems is actually a reference to an array in memory. Even though the array contents have changed, the value of action.adItems is still the same, a pointer to where the array is currently stored. This is the reason why your store is not being updated: as Redux does not check the values of the array itself but the reference to where the array is stored, the new state you're returning is exactly the same, so Redux is not aware of any changes.
As LonelyPrincess says, I was making this issue elsewhere, if you doing that xArray = yArra it means call by reference and not by value.

Vuex Getter Undefined

I am new to Vue.js and experiencing an issue with Vuex modules and Axios. I have a "post" component that retrieves a slug from the router and fetches data with Axios which is then retrieved with Vuex Getters.
I am able to retrieve data successfully but then I still see this error on my DevTools, "TypeError: Cannot read property 'name' of undefined"
Due to this error I am not able to pass this.post.name to Vue-Meta.
Codes
Post.vue
computed: {
...mapGetters(["post"]),
},
mounted() {
const slug = this.$route.params.slug;
this.fetchPost({ slug: slug });
},
methods: {
...mapActions(["fetchPost"]),
/store/modules/post.js
const state = {
post: [],
};
const getters = {
post: (state) => {
return post;
}
};
const actions = {
async fetchPost({ commit }, arg) {
try {
await axios.get("/post/" + arg.slug).then((response) => {
commit("setPost", response.data);
});
} catch (error) {
console.log(error);
}
},
};
const mutations = {
setPost: (state, post) => (state.post = post),
};
export default {
state,
getters,
actions,
mutations,
};
Your getter is utterly wrong: a state getter is supposed to be a function that takes in the entire state as a param and retrieves whatever you're interested in from it. Your version...
const getters = {
post: (state) => {
return post;
}
};
...takes in the state as a param but doesn't use it. Instead, it returns a variable (post) which has not been defined in that context.
Which will always return undefined, regardless of current value of state.post.
And, as you already know, JavaScript can't access property 'name' of undefined.
To get the current value of state.post, use:
const getters = {
post: state => state.post
}
Or
const getters = {
post: (state) => { return state.post; }
}
... if you fancy brackets.
Also, out of principle, I suggest initializing your post with an empty object {} instead of an empty array [].
Changing variable types as few times as possible is a very good coding habit, providing huge benefits in the long run.
Edit (after [mcve])
You have a bigger problem: the import from your axios plugin returns undefined. So you can't call get on it. Because you wrapped that call into a try/catch block, you don't get to see the error but the endpoint is never called.
I don't know where you picked that plugin syntax from but it's clearly not exporting axios. Replacing the import with import axios from 'axios' works as expected.
Another advice would be to namespace your store module. That's going to become useful when you'll have more than one module and you'll want to specifically reference a particular mutation/action on a specific module. You'll need to slightly change mapActions and mapGetters at that point.
See it working here.

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

Dispatch Redux action after React Apollo query returns

I'm using React Apollo to query all records in my datastore so I can create choices within a search filter.
The important database model I'm using is Report.
A Report has doorType, doorWidth, glass and manufacturer fields.
Currently when the query responds, I'm passing allReports to multiple dumb components which go through the array and just get the unique items to make a selectable list, like so..
const uniqueItems = []
items.map(i => {
const current = i[itemType]
if (typeof current === 'object') {
if (uniqueItems.filter(o => o.id !== current.id)) {
return uniqueItems.push(current)
}
} else if (!uniqueItems.includes(current)) {
return uniqueItems.push(current)
}
return
})
Obviously this code isn't pretty and it's a bit overkill.
I'd like to dispatch an action when the query returns within my SidebarFilter components. Here is the query...
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
})
})
const mapStateToProps = ({
reportFilter: { isPublished }
// filterOptions: { doorWidths }
}) => ({
isAssessment
// doorWidths
})
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
resetFilter,
saveFilter,
setDoorWidths,
handleDoorWidthSelect
},
dispatch
)
export default compose(connect(mapStateToProps, mapDispatchToProps), withData)(
Filter
)
The Redux action setDoorWidths basically does the code above in the SidebarFilter component but it's kept in the store so I don't need to re-run the query should the user come back to the page.
It's very rare the data will update and the sidebar needs to change.
Hopefully there is a solution using the props argument to the graphql function. I feel like the data could be taken from ownProps and then an action could be dispatched here but the data could error or be loading, and that would break rendering.
Edit:
Query:
query ($isPublished: Boolean!){
allReports(filter:{
isPublished: $isPublished
}) {
id
oldId
dbrw
core
manufacturer {
id
name
}
doorWidth
doorType
glass
testBy
testDate
testId
isAssessment
file {
url
}
}
}
While this answer addresses the specific issue of the question, the more general question -- where to dispatch a Redux action based on the result of a query -- remains unclear. There does not, as yet, seem to be a best practice here.
It seems to me that, since Apollo already caches the query results in your store for you (or a separate store, if you didn't integrate them), it would be redundant to dispatch an action that would also just store the data in your store.
If I understood your question correctly, your intent is to filter the incoming data only once and then send the result down as a prop to the component's stateless children. You were on the right track with using the props property in the graphql HOC's config. Why not just do something like this:
const mapDataToProps = ({ data = {} }) => {
const items = data
const uniqueItems = []
// insert your logic for filtering the data here
return { uniqueItems } // or whatever you want the prop to be called
}
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
}),
props: mapDataToProps,
})
The above may need to be modified depending on what the structure of data actually looks like. data has some handy props on it that can let you check for whether the query is loading (data.loading) or has errors (data.error). The above example already guards against sending an undefined prop down to your children, but you could easily incorporate those properties into your logic if you so desired.

Categories

Resources