Saving to redux store by key, nested state - javascript

Chat application:
I would like to save different ChatMessages arrays per conversation id.
I've imagined the state would look something like this:
state {
messages: {
isRequesting: false,
messageByConversationId: {
"23523534543": [messages],
"64634523453": [messages],
}
}
}
But I can't seem to save a nested state, is it possible?
My code: (That does not run because of this line: chatMessage[conversationId]: payload.chatMessages)
export const loadChatMessagesSuccess: Reducer<ImmutableChatMessagesState> =
(state: ImmutableChatMessagesState, {payload}: AnyAction & { payload?: LoadChatMessagesSuccessParams }) =>
payload ? {...state, requesting: false, chatMessage[conversationId]: payload.chatMessages}
: state;

Not sure I fully understand your question. But you could do the following to add loaded messages to the messageByConversationId object while keeping other loaded messages.
NB. This assumes you have somehow get conversationId as a variable. For example you could make it to be a part of payload
export const loadChatMessagesSuccess: Reducer<ImmutableChatMessagesState> = (
state: ImmutableChatMessagesState,
{ payload }: AnyAction & { payload?: LoadChatMessagesSuccessParams }
) =>
payload
? {
...state,
requesting: false,
messageByConversationId: {
...state.messageByConversationId,
[payload.conversationId]: payload.chatMessages
}
}
: state;

Related

Understanding how to use preloadedState in redux-toolkit

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

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

JHipster and React: How to call function after calling asynchronous reducer action?

Use Case: I have a react application generated with JHipster. I need to get data from API, map to form contract, and then submit the form.
Problem: JHipster generated reducer code doesn't return a promise, so how do I know when a reducer action is finished? How can I call a function after getting an entity to update state?
Getting Entity and Returning the ICrudGetAction:
export interface IPayload<T> { // redux-action-type.ts (JHipster Code from node_modules)
type: string;
payload: AxiosPromise<T>;
meta?: any;
}
export type IPayloadResult<T> = ((dispatch: any) => IPayload<T> | Promise<IPayload<T>>);
export type ICrudGetAction<T> = (id: string | number) => IPayload<T> | ((dispatch: any) => IPayload<T>);
// Start of my reducer code
export default (state: AdState = initialState, action): AdState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.FETCH_MYENTITY_LIST):
return {
...state,
errorMessage: null,
updateSuccess: false,
loading: true
};
case FAILURE(ACTION_TYPES.FETCH_MYENTITY_LIST):
return {
...state,
loading: false,
updating: false,
updateSuccess: false,
errorMessage: action.payload
};
case SUCCESS(ACTION_TYPES.FETCH_MYENTITY_LIST): {
const links = parseHeaderForLinks(action.payload.headers.link);
return {
...state,
loading: false,
links,
entities: loadMoreDataWhenScrolled(state.entities, action.payload.data, links),
totalItems: parseInt(action.payload.headers['x-total-count'], 10)
};
}
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
export const getEntity: ICrudGetAction<IMyEntity> = id => {
const requestUrl = `${apiUrl}/${id}`;
return {
type: ACTION_TYPES.FETCH_ENTITY,
payload: axios.get<IMyEntity>(requestUrl)
};
};
I try to do this and it works but gives me compile errors:
this.props.getEntity(this.props.match.params.id).then((response) => {
// Map to form attributes and call setState
});
I get error message:
TS2339: Property 'then' does not exist on type 'IPayload | ((dispatch: any) => IPayload)'.
Property 'then' does not exist on type 'IPayload'.
This makes sense because we are not returning a promise. but how can I update the code to return a promise such that I don't break all the things that were autogenerated while also keeping the redux store updated?

Vuex Mutation running, but component not updating until manual commit in vue dev tools

I have a vue component that I can't get to update from a computed property that is populated from a service call.
Feed.vue
<template>
<div class="animated fadeIn">
<h1 v-if="!loading">Stats for {{ feed.name}}</h1>
<h2 v-if="loading">loading {{ feedID }}</h2>
</div>
</template>
<script>
export default {
data: () => {
return {
feedID: false
}
},
computed: {
feed(){
return this.$store.state.feed.currentFeed
},
loading(){
return this.$store.state.feed.status.loading;
}
},
created: function(){
this.feedID = this.$route.params.id;
var fid = this.$route.params.id;
const { dispatch } = this.$store;
dispatch('feed/getFeed', {fid});
}
}
</script>
That dispatches 'feed/getFeed' from the feed module...
feed.module.js
import { feedStatsService } from '../_services';
import { router } from '../_helpers';
export const feed = {
namespaced: true,
actions: {
getFeed({ dispatch, commit }, { fid }) {
commit('FeedRequest', {fid});
feedStatsService.getFeed(fid)
.then(
feed => {
commit('FeedSuccess', feed);
},
error => {
commit('FeedFailure', error);
dispatch('alert/error', error, { root: true });
}
)
}
},
mutations: {
FeedRequest(state, feed) {
state.status = {loading: true};
state.currentFeed = feed;
},
FeedSuccess(state, feed) {
state.currentFeed = feed;
state.status = {loading: false};
},
FeedFailure(state) {
state.status = {};
state.feed = null;
}
}
}
The feedStatsService.getFeed calls the service, which just runs a fetch and returns the results. Then commit('FeedSuccess', feed) gets called, which runs the mutation, which sets state.currentFeed=feed, and sets state.status.loading to false.
I can tell that it's stored, because the object shows up in the Vue dev tools. state.feed.currentFeed is the result from the service. But, my component doesn't change to reflect that. And there is a payload under mutations in the dev tool as well. When manually commit feed/feedSuccess in the dev tools, my component updates.
What am I missing here?
In the same way that component data properties need to be initialised, so too does your store's state. Vue cannot react to changes if it does not know about the initial data.
You appear to be missing something like...
state: {
status: { loading: true },
currentFeed: {}
}
Another option is to use Vue.set. See https://vuex.vuejs.org/guide/mutations.html#mutations-follow-vue-s-reactivity-rules...
Since a Vuex store's state is made reactive by Vue, when we mutate the state, Vue components observing the state will update automatically. This also means Vuex mutations are subject to the same reactivity caveats when working with plain Vue
Hey for all the people coming to this and not being able to find a solution. The following was what worked for me:
Declaring base state:
state: {
mainNavData: [],
}
Then I had my action which is calling the now fixed mutation:
actions : {
async fetchMainNavData({ commit }) {
var response = await axios.get();
commit('setMainNavData', response));
},
};
Now my mutation is calling this updateState() function which is key to it all
mutations = {
setMainNavData(state, navData) {
updateState(state, 'mainNavData', navData);
},
};
This is what the updateState function is doing which solved my issues.
const updateState = (state, key, value) => {
const newState = state;
newState[key] = value;
};
After adding updateState() my data reactively showed up in the frontend and I didn't have to manually commit the data in Vue tools anymore.
please note my store is in a different file, so its a little bit different.
Hope this helps others!
Sometimes updating property that are not directly in the state is the problem
{
directprop: "noProblem",
indirectParent: {
"test": 5 // this one has a problem but works if we clone the whole object indirectParent
}
}
but it is a temporary solution, it should help you to force update the state and discover what is the real problem.

redux how to get or store computed values

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

Categories

Resources