Avoiding repeating state names - javascript

Let's say i have a rootreducer like below.
const rootR = combineReducers({
topics,
...
});
the topics reducer
function topics(state = { topics=[], error: null}, action){
switch (action.type){
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
topics,
};
default:
return state;
}
};
And when i fire the related action i get my state with repeatable properties state.topics.topics instead of state.topics
Is there any way to avoid this repeating (topics.topics)?
Thanks in advance

Looking at the initialState of your topics reducer, the state object accessible to topics reducer has this structure:
{
topics: [],
error: null
}
So when you combineReducers like this:
const rootR = combineReducers({
topics,
anotherReducer,
someOtherReducer.
// ...
});
resulting global app state is going to look like this:
{
topics: {
topics: [],
error: null
},
anotherReducer: {
// ...
},
someOtherReducer: {
// ...
},
// ...
}
So if you want to access topics array from global state, you need to do state.topics.topics.
You have two things under state.topics, an array of topics and error.
Hence let's rename second topics key to items to avoid confusion.
(it is unavoidable to have a second key to store the array because you also want error)
thus we have:
state.topics = {
items: [],
error: null,
}
Instead of state.topics.topics, now we access state.topics.items
To achieve this, initialstate passed to topics reducer has to be:
function topics(state = { items = [], error: null }, action){
//...
}
Now inside the reducer FETCH_TOPICS_SUCCESSFULLY, we want to append an array action.topics to items, like this (without mutating our current state):
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
items: [
...state.items,
...topics
],
};

#markerikson is right, the state variable passed in the function is actually topics once FETCH_TOPICS_SUCCESSFULLY is called, so it's better to do return topics there.
But given your condition, instead of return {...state, topics} or return topics, you can also do return Object.assign({}, state, topics). This will create a new object with all properties from previous state and topics merged together.

You're double-nesting things. The topics reducer will only see the "topics" slice of state. So, instead of returning {...state, topics}, just do return topics.
update
Your edit to the question changes the situation considerably.
Originally, you had:
function topics(state = {}, action){
Now, you have:
function topics(state = { topics=[], error: null}, action){
I'll admit I'm a bit confused at this point as to what your desired state structure actually should be.
Looking at your original definition, it seemed like you were misunderstanding how combineReducers works, and redundantly trying to return a new object that contained a field/slice named "topics". Now, it looks like the root-level "topics" slice itself has a field named "topics" as well.
Are topics and error supposed to be at the root of your state tree? Or, are they both really supposed to be part of the top-level "topics" slice? If that's really what you want, then you've defined the state tree as needing to be topics.topics.
Also, to answer #free-soul: no, in the original example, return topics would not mutate state, because it's just returning whatever was in the action. Even if the action.topic field was literally the same array that used to be in the state, the result would just be a no-op.

Related

NgRx Select Errors When Attempting Access on Nested Properties

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

Reducer not returning the expected empty object

Using React-redux here and having a bit of an issue, that some of you might help with.
The user can create 'Jobs' (posts) and also remove them. Adding them is no issue and the reducer returns what is expected. However, once I delete a job from the (firebase) database I trigger a new fetch for the current jobs, but the reducer still returns the old jobs. Am I missing something?
Before deleting, this is how the jobs objects looks like:
activeJobs= {
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
},
-KrkPPy4ibSraKG-O49S: {
title: 'Help',
location: 'etc,
...
} and so on
}
When I delete them all I get this {} back from the server. Expected.
What is not expected is that my reducer still returns the old jobs and my components do not re-render.
I dispatch an action after fetching the jobs:
firebase.database().ref(`/jobs/activeJobs/${currentUser.uid}`)
.on('value', snapshot => {
console.log('new activeJobs ===', snapshot.val());
dispatch({
type: FETCH_JOBS_SUCCESS,
payload: snapshot.val()
});
});
snapshot.val() does contain the new updated jobs.
Then here is the reducer that handles the action:
switch (action.type) {
case FETCH_JOBS_SUCCESS:
// ...state contains the OLD jobs and action.payload contains {}. Why is is not overriding it the old jobs?
return { ...state, ...action.payload };
default:
return state;
}
Why is my reducer failing?
The { ...state, ...action.payload } syntax actually mean : build a new object by taking every prop of state and adding every props of action.payload. In your case, you just get a new object that is similar to state, since ...action.payload is an empty object.
Change your action to
return Object.assign({}, state, {activeJobs : action.payload});

Ngrx observables not reacting to state store change when using Object.assign() in combination with other operations

I have some trouble understanding why the first sample seems to work ok but the second one has trouble firing the observers.
// Working reducer
return Object.assign({}, state, {
expanded: Object.assign({}, state.expanded, { clients: !state.expanded.clients })
});
// Faulty reducer - Devtools indicates a change but the observables
// seem to not respond and pass on the information down the line
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
// Selectors
import {createSelector} from 'reselect';
export const SIDEBAR = (state: AppState) => state.sidebar;
export const SIDEBAR_UI = createSelector<AppState, SidebarUIState, SidebarState>(
SIDEBAR,
(state: SidebarState) => state.ui
);
// Sidebar service
public getSidebarUIExpandedObservable(): Observable<SidebarUIExpandedState> {
debug('Get sidebar UI expanded observable');
return this._store.select(SIDEBAR_UI_EXPANDED);
}
Instead of doing :
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
do
return Object.assign(
{},
state,
{
expanded: Object.assign(
{},
state.expanded,
{
clients: !state.expanded.clients
}
)
}
);
Trick :
When you'll have Typescript >= 2.1.4 you'll be able to do it like that
return {
...state,
{
expanded: {
...state.expanded,
{
clients: !state.expanded.clients
}
}
}
};
Reducers require pure functions, that make use of immutable objects.
Hence you cannot mutate the newState object, but you would have to create a new object that has definite new state upon creation.
Your first example fits the immutability principle, because Object.assign copies the matching propertie(s) of the object in the third parameter to a copy of the object in the second parameter, before assigning this to target object (first parameter). Thus, the original state has remained unaffected, and a new state is obtained in the target object.
In other words, no original object state has been changed by object.assign in memory and the return value delivers a ready to go (no further mutations required ) new state object.
more info on Object.assign(): https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
In the second case there is a new object created at first (immutability still valid), subsequently, the state is mutated by assigning the boolean, hence the immutability principle is broken, and reducers usually do not accept this.
With the following in a reducer:
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
the content of state.expanded is being mutated.
That is newState.expanded and state.expanded refer to the same object and that object's client property is being mutated. Code looking at expanded will see that the object reference is unchanged and will assume that the content has not changed either, but you have mutated it by toggling the value of its clients property.

Pattern for updating multiple parts of Redux state

The shape of my Redux state looks like this:
{
user: {
id: 123,
items: [1, 2]
},
items: {
1: {
...
},
2: {
...
}
}
}
Using combineReducers I have 2 sets of reducers. Each act on one of the root keys of the state. i.e. one manages the user key and the other the items key.
If I want to add an item I can call 2 reducers, the first will add a new object to the items and the second will add the id to the user.items array.
This has a bad code smell. I feel that there should be a way to atomically reduce the state of both objects at the same time. i.e. in addition to the sub-reducers have a root reducer that acts on the root object. Is this possible?
I think what you're doing is actually correct!
When dispatching an action, starting from the root-reducer, every "sub-reducer" will be called, passing the corresponding "sub-state" and action to the next layer of sub-reducers. You might think that this is not a good pattern since every "sub-reducer" gets called and propagates all the way down to every single leaf node of the state tree, but this is actually not the case!
If the action is defined in the switch case, the "sub-reducer" will only change the "sub-state" part it owns, and maybe passes the action to the next layer, but if the action isn't defined in the "sub-reducer", it will do nothing and return the current "sub-state", which stops the propagation.
Let's see an example with a more complex state tree!
Say you use redux-simple-router, and I extended your case to be more complex (having data of multiple users), then your state tree might look something like this:
{
currentUser: {
loggedIn: true,
id: 123,
},
entities: {
users: {
123: {
id: 123,
items: [1, 2]
},
456: {
id: 456,
items: [...]
}
},
items: {
1: {
...
},
2: {
...
}
}
},
routing: {
changeId: 3,
path: "/",
state: undefined,
replace:false
}
}
As you can see already, there are nested layers in the state tree, and to deal with this we use reducer composition, and the concept is to use combineReducer() for every layer in the state tree.
So your reducer should look something like this:
(To illustrate the layer by layer concept, this is outside-in, so the order is backwards)
first layer:
import { routeReducer } from 'redux-simple-router'
function currentUserReducer(state = {}, action) {
switch (action.type) {...}
}
const rootReducer = combineReducers({
currentUser: currentUserReducer,
entities: entitiesReducer, // from the second layer
routing: routeReducer // from 'redux-simple-router'
})
second layer (the entities part):
function usersReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
case TYPE_TWO:
case TYPE_TREE:
return Object.assign({}, state, {
// you can think of this as passing it to the "third layer"
[action.userId]: itemsInUserReducer(state[action.userId], action)
})
case TYPE_FOUR:
return ...
default:
return state
}
}
function itemsReducer(...) {...}
const entitiesReducer = combineReducers({
users: usersReducer,
items: itemsReducer
})
third layer (entities.users.items):
/**
* Note: only ADD_ITEM, TYPE_TWO, TYPE_TREE will be called here,
* no other types will propagate to this reducer
*/
function itemsInUserReducer(state = {}, action) {
switch (action.type) {
case ADD_ITEM:
return Object.assign({}, state, {
items: state.items.concat([action.itemId])
// or items: [...state.items, action.itemId]
})
case TYPE_TWO:
return DO_SOMETHING
case TYPE_TREE:
return DO_SOMETHING_ELSE
default:
state:
}
}
when an action dispatches
redux will call every sub-reducer from the rootReducer,
passing:
currentUser: {...} sub-state and the whole action to currentUserReducer
entities: {users: {...}, items: {...}} and action to entitiesReducer
routing: {...} and action to routeReducer
and...
entitiesReducer will pass users: {...} and action to usersReducer,
and items: {...} and action to itemsReducer
why is this good?
So you mentioned is there a way to have the root reducer handling different parts of the state, instead of passing them to separate sub-reducers. But if you don't use reducer composition and write a huge reducer to handle every part of the state, or you simply nest you state into a deeply nested tree, then as your app gets more complicated (say every user has a [friends] array, or items can have [tags], etc), it will be insanely complicated if not impossible to figure out every case.
Furthermore, splitting reducers makes your app extremely flexible, you just have to add any case TYPE_NAME to a reducer to react to that action (as long as your parent reducer passes it down).
For example if you want to track if the user visits some route, just add the case UPDATE_PATH to your reducer switch!

React-redux store updates but React does not

Bear with me here as this question pertains to my first test app using either React, Redux or react-redux. Docs have gotten me far and I have a mock banking app that mostly works. My state object looks roughly like this:
{
activePageId: "checking",
accounts: [
checking: {
balance: 123,
transactions: [
{date, amount, description, balance}
]
}
]
}
I have just two actions:
1. CHANGE_HASH (as in url hash). This action always works as expected and all the reducer does is update the state.activePageId (yes, I'm cloning the state object and not modifying it). After the action, I can see the state has changed in the Redux store and I can see that React has updated.
function changeHash(id) {
return {
type: "CHANGE_HASH",
id: id
}
}
2. ADD_TRANSACTION (form submission). This action never updates React, but it always updates the Redux store. The reducer for this action is updating state.accounts[0].balance and it's adding a transaction object to the array state.accounts[0].transactions. I don't receive any errors, React just doesn't update. HOWEVER, if I dispatch a CHANGE_HASH action React will catch up and display all of the ADD_TRANSACTION state updates properly.
function addTransaction(transaction, balance, account) {
return {
type: "ADD_TRANSACTION",
payload: {
transaction: transaction,
balance: balance,
account: account
}
}
}
My reducer...
function bankApp(state, action) {
switch(action.type) {
case "CHANGE_HASH":
return Object.assign({}, state, {
activePageId: action.id
});
case "ADD_TRANSACTION":
// get a ref to the account
for (var i = 0; i < state.accounts.length; i++) {
if (state.accounts[i].name == action.payload.account) {
var accountIndex = i;
break;
}
}
// is something wrong?
if (accountIndex == undefined) {
console.error("could not determine account for transaction");
return state;
}
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
return newState;
default:
return state;
}
My mapStateToProps
function select(state) {
return state;
}
What am I missing here? I'm under the impression that React is supposed to update as the Redux storeis updated.
Github repo:
Deployment bank demo
p.s. I lied about not having any errors. I do have a number of warnings
""Warning: Each child in an array or iterator should have a unique "key" prop..."
I'm already giving them a key prop set to it's index. I doubt that has anything to do with my issue though.
The problem is in this piece of code:
// clone the state
var newState = Object.assign({}, state);
// add the new transaction
newState.accounts[accountIndex].transactions.unshift(action.payload.transaction);
// update account balance
newState.accounts[accountIndex].balance = action.payload.balance;
Cloning the state object doesn't mean you can mutate the objects it is referring to. I suggest you to read more about immutability because this isn't how it works.
This problem and solution to it are described in detail in Redux “Troubleshooting” docs so I suggest you to read them.
https://redux.js.org/troubleshooting
I also suggest you to take a look at Shopping Card example in Flux Comparison for Redux because it shows how to update nested objects without mutating them in a similar way to what you are asking.
https://github.com/voronianski/flux-comparison/tree/master/redux

Categories

Resources