How should I implement in redux computed values based on it's current state?
I have this for an example a sessionState
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
export default function sessionReducer(state = defaultState, action) {
switch(action.type) {
case STORE_DATA:
return _.merge({}, state, action.data);
case PURGE_DATA:
return defaultState;
default:
return state;
}
}
Say for example I want to get if the session is logged in, I usually do right now is sessionState.data.id to check it, but I want to know how I can do sessionState.loggedIn or something?
Can this do?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
},
loggedIn: () => {
this.data.id
}
}
or something (that's not tested, just threw that in). In this example it looks simple to just write .data.id but maybe when it comes to computations already, it's not good to write the same computations on different files.
Adding methods to state object is a bad idea. State should be plain objects. Keep in mind, that some libraries serialize the app state on every mutation.
You can create computing functions outside the state object. They can receive state as an argument. Why should they be state's methods?
const defaultState = {
ui: {
loading: false
}, metadata: { },
data: {
id: null
}
}
const loggedIn = (state) => {
//your logic here
}
In your particular example, the calculated result is incredibly simple and fast to calculate so you can calculate it on demand each time. This makes it easy to define a function somewhere to calculate it like #Lazarev suggested.
However, if your calculations become more complicated and time consuming, you'll want to store the result somewhere so you can reuse it. Putting this data in the state is not a good idea because it denormalizes the state.
Luckily, since the state is immutable, you can write a simple pure function to calculate a result and then you can use memoization to cache the result:
const isLoggedIn = memoize(state => state.login.userName && state.login.token && isValidToken (state.login.token));
Lastly, if you want to use methods on your store state, you can use Redux Schema (disclaimer: I wrote it):
const storeType = reduxSchema.type({
ui: {
loading: Boolean
}, metadata: { },
data: {
id: reduxSchema.optional(String)
},
loggedIn() {
return Boolean(this.data.id);
}
});
Related
Im trying to save some things in locale storage and reHydrate the state on refresh.
And it works, but my initalState from my slice file is overwritten.
const reHydrateStore = () => {
if (localStorage.getItem('culture') !== null) {
return { login: { culture: JSON.parse(localStorage.getItem('culture')) } };
}
};
export default reHydrateStore;
Then in configureStore:
preloadedState: reHydrateStore(),
my initalState object looks like this when I dont use reHydrateStore:
const initialState = {
isLoading: false,
culture: '',
error: null,
};
And when I use reHydrateStore:
const initialState = {
culture: 'en-US',
};
Is there a way to configure the preloadedState to just replace the given prop?
I know I can set up all initalState props in the reHydrateStore method but that seems like awfaul way of doing it. Writing the same code twice feels very unnescesery imo.
Edit:
I moved the logic to the slice to be able to access the initalState.
But #Rashomon had a better approach imo and will try with that instead.
But here was my first solution:
const reHydrateLoginSlice = () => {
if (localStorage.getItem("culture")) {
return {
login: { ...initialState, culture: JSON.parse(localStorage.getItem("culture")) },
};
}
};
export { reHydrateLoginSlice };
Maybe you can define your initialState as this to avoid rewriting the rest of keys:
const initialState = {
isLoading: false,
culture: '', // default value, will be overwrited in following line if defined in the storage
...(reHydrateStore() || {}), // if returns undefined, merge an empty object and use previous culture value
error: null,
};
If you only need culture to be rehydrated, I would rehydrate only the value instead of an object to make it simpler:
const initialCultureValue = ''
const initialState = {
isLoading: false,
culture: reHydrateCulture() || initialCultureValue,
error: null,
};
I have a template that needs some non-reactive data in it from my Vuex store. However at the moment, I have to manually switch views to get the data to load. I am assuming I should not use mounted or created. If I use watch, then it basically becomes reactive again, and I only want to get this once.
data: function () {
return {
localreadmode: false,
myArray: null,
}
},
computed: {
...mapState({
myNodes: (state) => state.myNodes,
configPositions: (state) => state.configPositions,
configEmoji: (state) => state.configEmoji,
}),
nodes_filtered: function () {
return this.myNodes.filter((nodes) => {
return nodes.deleted == false
})
},
},
// this is to stop sync chasing bug
myArray: null,
created() {
this.$options.myArray = this.nodes_filtered
console.log(this.nodes_filtered)
// is empty unless I switch views
},
You could still use a watcher that runs only once via the vm.$watch API. It returns a method that can be called to stop watching the value, so your handler could invoke it when nodes_filtered[] is not empty.
export default {
mounted() {
const unwatch = this.$watch(this.nodes_filtered, value => {
// ignore falsy values
if (!value) return
// stop watching when nodes_filtered[] is not empty
if (value.length) unwatch()
// process value here
})
}
}
demo
When the page is being loaded for the first time, vue component is not waiting for my custom store file to process it. I thought it might fix it with promises but I am not sure on how to do so on functions that do not really require extra processing time.
I am not including the entire .vue file because I know it surely works just fine. My store includes couple of functions and it is worth mentioning it is not set up using vuex but works very similarly. Since I also tested what causes the issue, I am only adding the function that is related and used in MainComp.
Vue component
import store from "./store";
export default {
name: "MainComp",
data() {
return {
isLoading: true,
storageSetup: store.storage.setupStorage,
cards: Array,
};
},
created() {
this.storageSetup().then(() => {
this.cards= store.state.cards;
});
this.displayData();
},
methods: {
displayData() {
this.isLoading = false;
},
}
My custom store.js file
const STORAGE = chrome.storage.sync;
const state = {
cards: []
};
const storage = {
async setupStorage() {
await STORAGE.get(['cards'], function (data) {
if (Object.keys(data).length === 0) {
storage.addToStorage('ALL');
// else case is the one does not work as required
} else {
data.cards.forEach((elem) => {
// modifies the element locally and then appends it to state.cards
actions.addCard(elem);
});
}
});
}
};
export default {
state,
storage
};
Lastly, please ignore the case in setupStorage() when the length of data is equal to 0. If there is nothing in Chrome's local space, then a cards is added properly(state.cards is an empty array every time the page loads). The problem of displaying the data only occurs when there are existing elements in the browser's storage.
How can I prevent vue from assuming cards is not an empty array but instead wait until the the data gets fetched and loaded to state.cards (i.e cards in MainComp)?
Sorry if the problem can be easily solved but I just lost hope of doing it myself. If any more information needs to be provided, please let me know.
Your main issue is that chrome.storage.sync.get is an asynchronous method but it does not return a promise which makes waiting on it difficult.
Try something like the following
const storage = {
setupStorage() {
return new Promise(resolve => { // return a promise
STORAGE.get(["cards"], data => {
if (Object.keys(data).length === 0) {
this.addToStorage("All")
} else {
data.cards.forEach(elem => {
actions.addCard(elem)
})
}
resolve() // resolve the promise so consumers know it's done
})
})
}
}
and in your component...
export default {
name: "MainComp",
data: () => ({
isLoading: true,
cards: [], // initialise as an array, not the Array constructor
}),
async created() {
await store.storage.setupStorage() // wait for the "get" to complete
this.cards = store.state.cards
this.isLoading = false
},
// ...
}
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);
});
I was wondering if it is possible, to dispatch only a single property to vuex store instead the whole object. There are no possibilities written in vuex docs. Currently I store object like this:
store.dispatch('currentUser', {
memberData: 'fooBar'
});
Some times I just want to fetch only a single value from database and push it into store. Is there a way to do that?
UPDATE
I think my question is unclear.
Actually I need to access a child nested property of memberData in dispatch, to change only one element of memberData. To be honest, it does not matter what response is. It could be 'foo' as well.
If you review the documentation for dispatch, the 2nd argument payload, can be any type. It doesn't necessarily need to be an object-style dispatch nested in a property:
Dispatch:
store.dispatch('currentUser', response[0]);
Store:
state: {
currentUser: undefined
},
mutations: {
setCurrentUser(state, currentUser) {
state.currentUser = currentUser;
}
},
actions: {
currentUser(context, payload) {
context.commit('setCurrentUser', payload);
}
}
Mutation:
setCurrentUser(state, currentUser) {
state.currentUser = currentUser;
}
Here is a example in action.
Update:
If the goal instead is to merge/update changes, you can use spread in object literals. This is also mentioned in the Vuex documentation for Mutations Follow Vue's Reactivity Rules.
Dispatch:
store.dispatch('currentUser', { systemLanguage: response[0].systemLangId });
Store:
state: {
memberData: { systemLanguage: 'foo' }
},
mutations: {
updateCurrentUser(state, updates) {
state.memberData = { ...state.memberData, ...updates };
}
},
actions: {
currentUser(context, payload) {
context.commit('updateCurrentUser', payload);
}
}
Here is an example of that in action.
Hopefully that helps!