Redux toolkit serializableMiddleware doesn't work - google-cloud-firestore

I encountered with an issue of serializableMiddleware in redux toolkit. I need to store firestore timestamps inside my redux store and don't want that serializableMiddleware logged this text:
A non-serializable value was detected in the state, in the path: path to timestamp. Value: nt {seconds: > 1675422816, nanoseconds: 106000000}
Take a look at the reducer(s) handling this action.
So i take a look at the serializableMiddleware in redux toolkit docs and find out that there is an option isSerializable?: (value: any) => boolean. When I return true from this method, value should consider serializable, but still logs error out even if I always return true.
I wrote function but it didn't work:
const isSerializable = (value: any) =>
value instanceof Timestamp || isPlain(value);
const serializableMiddleware = createSerializableStateInvariantMiddleware({
isSerializable
})
Here is how I setup my store:
export const setupStore = () => {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(serializableMiddleware)
})
}

The problem here is that getDefaultMiddleware() already adds the serializable middleware as part of the default setup. You're creating a second instance of the middleware, but that doesn't change the first instance.
Instead, you need to customize the first instance by passing the right options to getDefaultMiddleware():
export const setupStore = () => {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
isSerializable: myCustomIsSerializableFunction
}
})
})
}

Related

How to import type definition from react-query to set type of options for useQuery hook?

I'm adding react-query to my automations pipe and need generating wrapper around the useQuery for various API calls. I want to expose all options of useQuery to developer using this wrapper. How to correctly define options type to get all the benefits of ts for those?
:any works but not giving autocomplete/checks then:
const usePosts = ({ page, perPage, workspaceId }: payload, ___options?: any) => {
If I add :UseQueryOptions, just to see the error of mismatch, error message is quite big and has a lot inside. Would love to find way to import that type definition from react-query and reuse it.
Just found it, sometimes all needed is to write a question 😅 Maybe someone else will benefit from this:
import { useQuery, QueryOptions } from "#tanstack/react-query";
import sdk from "../sdks/demo";
type payload = {
page?: number;
perPage?: number;
workspaceId: string;
};
const usePosts = (
{ page, perPage, workspaceId }: payload,
___options?: QueryOptions
) => {
let key = `posts-list-${workspaceId}-${page}-${perPage}`;
return useQuery(
[key],
() =>
sdk.postsList({
workspaceId,
page,
perPage,
}),
___options
);
};
export default usePosts;

Suspense returns data too fast in #tanstack/react-query v4

I am upgrading a React app from react-query v3 to #tanstack/react-query v4.
Almost everything works, but I'm having a problem with Suspense.
I have a react component:
const WrapperPageEdit: React.FC<MyProps> = ({
pageUuid,
redirect,
}: MyProps) => {
const FormPage = React.lazy(() => import('./FormPage'));
const { data } = usePageView(pageUuid);
if (data?.[0]) {
const pageObjectToEdit= data[0];
const content = pageObjectToEdit.myStuff.content;
return (
<Suspense
fallback={<Trans id="loading.editor">Loading the editor...</Trans>}
>
<FormPage
id={uuid}
content={content}
redirect={redirect}
/>
</Suspense>
);
}
return <p>No data.</p>;
};
And here's my query:
export function usePageView(
uuid: string,
): UseQueryResult<DrupalPage[], Error> {
return useQuery<DrupalPage[], Error>(
queryKeyUsePageView(uuid),
async () => {
return fetchAnon(getPageByPageUuid(uuid));
},
{
cacheTime: YEAR_MILLISECONDS,
staleTime: YEAR_MILLISECONDS,
onSuccess: (data) => {
if (data?.[0]) {
data.map((element) => processResult(element));
}
},
},
);
}
This works in v3 but fails in v4 with the following error:
TypeError: Cannot read properties of undefined (reading 'content')
The reason the property is undefined is because that property is set by the processing in onSuccess (data.map).
The issue appears to be that in v4, the component WrapperPageEdit is refreshed before onSuccess in the usePageView query has finished processing, whereas in v3, the component WrapperPageEdit is not refreshed until the onSuccess data.map is complete.
How can I correctly fix this? I can write some additional code to try to check whether the onSuccess data.map is complete, but since react-query handled this automatically in v3, I'd like to rewrite my code in v4 so that it is the same.
The problem is likely that you are mutating the data in onSuccess. Directly modifying data in callbacks is not a good idea. Instead, do your transformation for example directly in the queryFn:
async () => {
const data = fetchAnon(getPageByPageUuid(uuid));
if (data?.[0]) {
data.map((element) => processResult(element));
}
return data
},
other good places to do data transformation is e.g. the select option, but it should always happen in an immutable way, because otherwise, you are overwriting the cached data inadvertently. React prefers updates to be immutable.

replaceReducer on #reduxjs/toolkit

Although I'm newbie of redux as well as of RTK, I'm just starting redux project with RTK as if you start with Spring Boot not Spring these days.
I want to dynamically inject reducers on demand on redux toolkit(RTK). I realized that I need to keep track of current reducers to make it. I expected my store from RTK would have a reference to them, but unfortunately it seems it doesn't have such a property.
While I found this module that seems to do the job, but it seems I have to go back to the days before RTK was created to make it work.
import {createStore, createReducer, Slice, Reducer, AnyAction, combineReducers} from "#reduxjs/toolkit";
const Store = createStore<any, any, any, any>(createReducer({}, () => {}));
const reducers: {
[key: string]: Reducer<any, AnyAction>;
} = {};
export const injectReducer = (slice: Slice) => {
reducers[slice.name] = slice.reducer;
Store.replaceReducer(combineReducers(reducers));
};
Even more (maybe I just don't know the way) type definitions will go insane.
Is there any way to make this?
I had the same issue, and finally managed to fix it with a few lines;
First, I created the store:
const staticReducers = {
counter: counterReducerSlice,
};
export const store = configureStore({
reducer: staticReducers,
middleware: getDefaultMiddleware => [logger, ...getDefaultMiddleware()],
enhancers: compose([monitorReducerEnhancer]),
});
then making the async reducers directory:
store.asyncReducers = {};
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer;
store.replaceReducer(createReducer(store.asyncReducers));
};
function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
});
}
finally, in my code, every time I want to add a new reducer to my store, I call store.injectReducer:
store.injectReducer('reducerKey', theReducerSlice);

How do I define an action without having to implement a reducer for it?

I'm converting some existing redux code to the toolkit way. We have a lot of actions that trigger thunks (to load data from backend) but dont have a reducer. Our pattern being the load/success/fail triple. Basically only the success and fails need a reducer statement. How do I do this with the toolkit? Do I have to put in a reducer that just returns the unchanged state for the load actions?
With redux-toolkit you have a few options here...
1. Existing thunks + RTK actions
If you only need to update one slice of your store with the loaded data, you can create “success” and “fail” actions in the reducers property on that slice. Then, change your thunk to dispatch those instead of the old success/fail actions.
const slice = createSlice({
name: 'data',
initialState: {},
reducers: {
fetchDataSuccess(state, action) {
// Do something with the response
},
fetchDataError(state, action) {
// Do something with the error
}
}
}
const { fetchDataSuccess, fetchDataError } = slice.actions
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch(fetchDataSuccess(response.data)))
.catch(error => dispatch(fetchDataError(error))
}
export default slice.reducer
2. Existing thunks + extraReducers
If you don't want to refactor the existing thunk, or if the actions will be used across multiple slices, you can use the extraReducers property.
// These can also be defined in a separate file and imported
const FETCH_SUCCESS = 'data/FETCH_SUCCESS'
const FETCH_FAIL = 'data/FETCH_FAIL'
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch({ type: FETCH_SUCCESS, payload: response.data }))
.catch(error => dispatch({ type: FETCH_FAIL, payload: error }))
}
const slice = createSlice({
// ... the usual properties
extraReducers: {
[FETCH_SUCCESS](state, action) {
// Do something with the response
},
[FETCH_FAIL](state, action) {
// Do something with the error
}
}
}
3. createAsyncThunk
This approach is similar to the above, but the createAsyncThunk utility handles a lot of it for you, like catching errors, dispatching the actions at the right time, etc.
const fetchData = createAsyncThunk(
'data/fetchData',
() => api.getData().then(response => response.data)
)
const slice = createSlice({
// ... the usual properties
extraReducers: {
[fetchData.fulfilled](state, action) {
// Do something with the response
},
[fetchData.rejected](state, action) {
// Do something with action.error
}
}
}
// Components still call this like a normal function: fetchData()
export { fetchData }
export default slice.reducer
Whichever way you end up going, if you're not using the "load" action (or .pending from createAsyncThunk), you don't need to add it to either reducers or extraReducers.
I think you can simply create thunk-actions.ts (or eg. saga-actions.ts) file to keep actions that trigger data loading.
import { createAction } from '#reduxjs/toolkit';
export const fetchUserComments = createAction<{ id: string }>(
'fetchUserComments',
);
all actions that have reducer's logic will be generated by slice

Custom Select() with parameter

UPDATE
As of #NGXS v3.1, they finally introduced arguments into #Selector().
https://www.ngxs.io/concepts/select#lazy-selectors
Examples from the DOCS
First, you define the #Selector "pandas"
#State<string[]>({
name: 'animals',
defaults: []
})
#Injectable()
export class ZooState {
#Selector()
static pandas(state: string[]) {
return (type: string) => {
return state.filter(s => s.indexOf('panda') > -1).filter(s => s.indexOf(type) > -1);
};
}
}
Then you just call it in your '.ts' file
import { Store } from '#ngxs/store';
import { map } from 'rxjs/operators';
#Component({ ... })
export class ZooComponent {
babyPandas$: Observable<string[]>;
constructor(private store: Store) {
this.babyPandas$ = this.store
.select(ZooState.pandas)
.pipe(map(filterFn => filterFn('baby')));
}
}
* From Old Post *
I am trying to create a custom #Select () to be able to drill down a particular tree and return the values dynamically. Getting either undefined or it's not making it (executing)
user.component.ts
const location = 'new york'
#Select(state => UserState.getUserLocationSlots(state, location)) slots$;
user.state.ts
#Selector()
static getUserLocationSlots(state: UserStateModel, location: any) {
console.log(state);
console.log(location); // <-- expecting 'new york', but getting undefined
}
You can achieve this by using crateSelector function from #ngxs/store
In your .state.ts file:
static getLocationSlots(location: string) {
return createSelector([UserState], (state: string[) => {
// logic for filtering your data
// eg.: state.filter(element => element == location)
})
}
In your .component.ts file:
#Select(UserState.getLocationSlots('new york')) slots$: Observable<any>
You can also check here for more details
I don't think it is possible to pass parameter to #Selector() decorated functions in ngxs v2. It would be nice though.
A ticket exist for this feature request.
Also, I think you are not using #Selector() correctly. I should be something like (hence, cannot pass parameters):
#Select(UserState.getUserLocationSlots) slots$
Refer to the docs.
Note: I am not an expert in ngxs...this is just based on what I understand now.
This is achievable in NGXS v2 & v3. Copied from my comment in the discussion on dynamic selectors here
We can achieve this at the moment using a pattern often used for redux
selectors...
The #Selector decorator can be written so that it returns a function
with the desired parameter. This enables the desired dynamic selector
arguments as well as late resolution of the selected state. For
Example:
#State<UserStateModel>( ... )
export class UserState {
#Selector()
getFilteredUsersFn(userStateModel: UserStateModel) {
return (filter: string) =>
userStateModel.users.filter((user) => user.indexOf(filter) >= 0);
}
}
And then the component would contain:
#Component({...})
export class AppComponent {
#Select(UserState.getFilteredUsersFn)
filteredUsersFn$: Observable<(filter: string) => User[]>;
get currentFilteredUsers$() {
return this.filteredUsersFn$
.pipe(map(filterFn => filterFn('myFilter')));
}
}
To pass parameters you can have the select return a function, it isn't elegant, however it works.
For example the select statement would look like:
#Selector()
static getItemByIdFn(state: { [id: number]: Entity }) {
return (id: number) => {
return state[id];
};
}
then in the component:
this.store.select(MyState.getItemByIdFn)
.pipe(map(mapByIdFn) => mayByIdFn(1)) // using the returned function
.subscribe(...);
Note the map, which is where you pass your id to the returned function. Here you can place whatever parameters you would like.
Hope this helps :)!