how do i update array of objects in Zustand? - react-hooks

i am trying to update the masterSteps variable inside the store but the store is undefined that's why it is saying MasterSteps is undefined but on similar way the setRoute method is working and routekey variable is getting set
const useStore = create((set) => ({
...initialState,
routeKey: "",
MasterSteps: [],
setRoute: (routeKeyToUse) => {
set((state) => {
state.routeKey = routeKeyToUse;
})
},
setMasterSteps: (o) => {
set((state) => {
state.MasterSteps= [...state.MasterSteps, o ]
})},
}));
NOTE : the o as parameter in setMasterSteps is having data
i am expecting how to setState the array in zustand store

Setter function should return new state, not imperativerly update it.
Try:
o => {
set((state) => ({ MasterSteps: [...state.MasterSteps, o ] }))
}
Edit:
Can you try:
const useStore = create((set) => ({
...initialState,
routeKey: "",
MasterSteps: [],
setRoute: (routeKeyToUse) => {
set((state) => ({
routeKey: routeKeyToUse;
}))
},
setMasterSteps: (o) => {
set((state) => ({
MasterSteps: [...state.MasterSteps, o ]
}))},
}));
Also what is definition of initialData? If it provides value for MasterSteps, it's overwritten by later MasterSteps = [].

Related

Redux RTK reloading state when calling getSelectors

I am new to Redux RTK so the problem might not exactly be on calling getSelectors(). However, when I'm using the state that comes from getSelectors() it reloads the entire state.
Problem
The baseline is that I have different Setup objects that I'm calling based on the documentId. These Setup objects are quite large so in the getSetups I am only fetching some basic properties. Then, when the user selects a specific Setup from the dropdown I want to save it in the setupSlice. But when I trigger the dispatch(setSetup(data)) the RTK reloads all the Setups.
I encounter an infinite loop when after fetching all the Setup objects I want to automatically assign the default Setup to the setupSlice.
Extra
Ideally when I assign a Setup to the setupSlice I would like to call the getSetup from RTK to fetch the entire Setup object of that specific Setup and store it in the setupSlice.
I am not sure if this is suppose to be happening but is there anyway to stop it? Otherwise is there any recommendation so I can move forward?
This is the component I'm trying to generate:
const SetupDropdown = () => {
const dispatch = useDispatch()
const { documentId } = useParams()
const { data, isFetching } = useGetSetupsQuery({ documentId })
let setupsMenu;
const { selectAll: selectAllSetups } = getSelectors({documentId})
const allSetups = useSelector(selectAllSetups)
if (!isFetching) {
const defaultSetup = allSetups.find((setup) => setup.default)
setupsMenu = allSetups.map(setup => {
return (<MenuItem value={setup.id}>{setup.name}</MenuItem>)
})
dispatch(setSetup(defaultSetup))
}
const setupId = useSelector(selectSetupId)
const handleChange = async (event) => {
// Here I ideally call the getSetup RTK Query to fetch the entire information of the single setup
const data = {
id: event.target.value,
name: 'Random name'
}
dispatch(setSetup(data))
};
return (
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Setup</InputLabel>
<Select
value={setupId}
onChange={handleChange}
label="Setup"
>
{setupsMenu}
</Select>
</FormControl>
)
}
export default SetupDropdown;
This is the setupApiSlice:
const setupsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState = setupsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: ({ documentId }) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
transformResponse: responseData => {
return setupsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, arg) => [
{ type: 'Setup', id: "LIST" },
...result.ids.map(id => ({ type: 'Setup', id }))
]
}),
getSetup: builder.query({
query: ({ documentId, setupId }) => ({
url: `/documents/${documentId}/setups/${setupId}`,
method: 'GET'
})
})
})
})
export const {
useGetSetupsQuery,
useGetSetupQuery
} = setupsApiSlice
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => setupsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
This is the setupSplice:
const initialState = {
name: null,
filters: [],
data: {},
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const setupSlice = createSlice({
name: 'setup',
initialState,
reducers: {
setSetup: (state, action) => {
console.log('Dispatch')
const setup = action.payload;
console.log(setup)
state.id = setup.id;
state.name = setup.name;
state.filters = setup.filters;
state.data = setup.state;
state.status = 'succeeded';
}
}
})
export const { setSetup } = setupSlice.actions;
export const selectSetupId = (state) => state.setup.id;
export const selectSetupName = (state) => state.setup.name;
export const selectSetupFilters = (state) => state.setup.filters;
export const selectSetupData = (state) => state.setup.data;
export default setupSlice.reducer;
Tbh., you probably should be using selectFromResult in your useGetSetupsQuery instead of adding another useSelector hook. That would also reduce your code complexity by a lot.
Your problem as hand is that you are creating those selectors within your component on each render - so they don't have a chance to actually memoize and give you a stable result. If you do that in your component, wrap it in a useMemo call to keep your selector instances as stable as possible.

How to getSelectors through passing arg in endpoint.select() in Redux RTK

I currently have this piece of code.
const initialState = documentsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: (documentId) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
providesTags: ['Setup']
}),
})
})
export const {
useGetSetupsQuery,
useAddSetupsMutation,
useUpdateSetupsMutation,
useDeleteSetupsMutation
} = apiSlice
And now I want to make use of the of the getSelector and do something like this (not implemented).
export const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select()
// Creates memoized selector
const selectSetupsData = createSelector(
selectSetupsResult,
setupsResult => setupsResult.data // normalized state object with ids & entities
)
export const {
selectAll: selectAllSetups,
selectById: selectSetupById,
selectIds: selectSetupIds,
} = setupsAdapter.getSelectors(state => selectSetupsData(state) ?? initialState)
The problem that I encounter is that endpoint.select() needs an argument in my case so that I can call setups on the correct documentId. I know I could just call all the setups and then filter out the ones that have the same documentId, but I was wondering if there is any other way. Even if it means not calling the endpoints.select() and still being able to use the getSelectors().
const initialState = documentsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: (documentId) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
// Add transformResponse
transformResponse: (responseData) => {
return documentsAdapter.setAll(initialState, responseData)
},
providesTags: ['Setup']
}),
})
})
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => documentsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
Then you can use it in a component as:
const { isFetching } = useGetSetupsQuery({id: 1})
// Dinamically get selectors based on parent query
const { selectAll: selectAllFromId1, selectById: selectByIdFromId1 } = getSelectors({id: 1})
// Use selectors based on parent id 1
const allFromId1 = useSelector(selectAllFromId1)
const setup1fromId1 = useSelector(selectByIdFromId1(5)) // get id 5

AlpineJS can not use magic method $watch

Having the following AlpineJS code and trying to use the magic method $watch, the code will fail with ReferenceError: $watch is not defined
window.experts = {
apiUrl: 'http://test.local:8991/api/',
data: [],
list: [],
expertsForm: null,
expertType: 'all',
queryUrl: '',
currentPage: 1,
sortByName: 'asc',
sortByZip: 'asc',
q: '',
fetchStatus: 'loading...',
retrieveList: () => {
const membersUrl = `${experts.apiUrl}members?include=user,association,affiliate`;
$watch('specialistType', (value) => console.log(value) );
experts.apiCalls(membersUrl)
},
setExpertType: (type) => {
console.log(type)
},
apiCalls: (url) => {
const response = fetch(url).then(res => {
if (!res.ok) {
experts.fetchStatus = 'error'
}
return res.json()
}).then(result => {
experts.list = result.data;
experts.data = result;
experts.fetchStatus = 'idle'
});
}
}
what goes wrong in this case?
Try accessing it via this. So it should be this.$watch(value, callback).
You should not use arrow function and add this
retrieveList(){
const membersUrl = `${experts.apiUrl}members?include=user,association,affiliate`;
this.$watch('specialistType', (value) => console.log(value) );
experts.apiCalls(membersUrl)
},

How to get data from failed forkJoin request?

Using Angular Rxjs and ngrx
I have an action that dispatch 4 API and I am doing the following =>
#Effect()
getAllModels$ = this.actions$.pipe(
ofType<featureActions.GetAllModelsRequest>(featureActions.ActionTypes.GetAllModelsRequest),
switchMap((action) =>
forkJoin([
this.dataService.GetAllModelFromServer(),
this.dataService.GetAllModelFromHost(),
this.dataService.GetAllModelFromCache(),
this.dataService.GetAllModelFromPreference(),
]).pipe(
map(
([server, host, cache, preference]) =>
new featureActions.GetAllModelsSuccess({
//...
})
),
catchError((error: HttpErrorResponse) => {
return of(new featureActions.GetAllModelsFailed({ error: error.message }));
})
)
)
);
The problem is, when one of those API fail, everything fail and I am in fail action. all the data that got retrieved (before the one endpoint that failed) is lost.
Is there a way to get the data retrieved in the catchError or the only solution is to chain the api one after the other ?
You can write your own implementation of forkJoin. Here is a simple example sourced from the original (https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/forkJoin.ts):
export function forkJoin2(...args: any[]): Observable<any> {
const resultSelector = popResultSelector(args);
const { args: sources, keys } = argsArgArrayOrObject(args);
if (resultSelector) {
// deprecated path.
return forkJoinInternal(sources, keys).pipe(map((values: any[]) => resultSelector!(...values)));
}
return forkJoinInternal(sources, keys);
}
function forkJoinInternal(sources: ObservableInput<any>[], keys: string[] | null): Observable<any> {
return new Observable((subscriber) => {
const len = sources.length;
if (len === 0) {
subscriber.complete();
return;
}
const values = new Array(len);
let completed = 0;
let emitted = 0;
for (let sourceIndex = 0; sourceIndex < len; sourceIndex++) {
const source = innerFrom(sources[sourceIndex]);
let hasValue = false;
subscriber.add(
source.subscribe({
next: (value) => {
if (!hasValue) {
hasValue = true;
emitted++;
}
values[sourceIndex] = value;
},
error: (err) => { return subscriber.error({ error: err, values }) },
complete: () => {
completed++;
if (completed === len || !hasValue) {
if (emitted === len) {
subscriber.next(keys ? keys.reduce((result, key, i) => (((result as any)[key] = values[i]), result), {}) : values);
}
subscriber.complete();
}
},
})
);
}
});
}
Notice, when an error occurs, you are returning the error along with the values:
error: (err) => { return subscriber.error({ error: err, values }) }
I went with this solution found here : https://medium.com/better-programming/rxjs-error-handling-with-forkjoin-3d4027df70fc
#Effect()
getAllModels$ = this.actions$.pipe(
ofType<featureActions.GetAllModelsRequest>(featureActions.ActionTypes.GetAllModelsRequest),
switchMap((action) =>
forkJoin([
this.dataService.GetAllModelFromServer().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromHost().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromCache().pipe(catchError(() => of({ data: [] }))),
this.dataService.GetAllModelFromPreference().pipe(catchError(() => of({ data: [] }))),
]).pipe(
map(
([server, host, cache, preference]) =>
new featureActions.GetAllModelsSuccess({
//...
})
),
catchError((error: HttpErrorResponse) => {
return of(new featureActions.GetAllModelsFailed({ error: error.message }));
})
)
)
);

How to call multiple mutations at the same time?

I have an array of ids, and I created a mutation that allow me to delete an item using only 1 id. Is there any way to call this mutation multiple times using Relay.Store.commitUpdate or this.props.relay.commitUpdate ?
I think you can wrap each Relay.Store.commitUpdate in Promise:
commitMutationPromise = (Mutation, data) =>
new Promise((resolve, reject) => {
Relay.Store.commitUpdate(new Mutation(data), {
onSuccess: (transaction) => {
resolve(transaction);
},
onFailure: (transaction) => {
reject(transaction);
},
});
}
And commit your mutations as array of promises and catch result with Promise.all(but keep in mind its fail-fast behaviour).
It could be something like this:
handleDelete = (deleteIds) => {
const deletePromisesArray = [];
deleteIds.forEach(id => {
deletePromisesArray.push(
this.commitMutationPromise(DeleteMutation, { id })
);
});
Promise.all(deletePromisesArray).then(values => {
this.onSuccessDelete(result);
}, error => {
this.onFailDelete(error);
});
}

Resources