redux-toolkit state change in extraReducer does not initiate rerender - react-native

I am trying to logout and purge the store at the same time, so on click I dispatch this:
dispatch({type: PURGE, key: 'root', result: () => { } });
Redux persist catches it, and reports purging the store. Great.
In another reducer I catch that dispatch, and remove my access token like this:
import { PURGE } from 'redux-persist/es/constants';
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setAccessToken(state: AuthState, action: PayloadAction<Auth>): void {
state.accessToken = action.payload.accessToken;
state.expiresIn = action.payload.expiresIn;
},
},
extraReducers: {
[PURGE]: (state: AuthState, action: string): void => {
state.accessToken = initialState.accessToken;
state.expiresIn = initialState.expiresIn;
},
},
});
The PURGE reducer actually is called, and modifies the state, but still no re-rendering happens. so redux must not pick that up. But according to the docs the Redux toolkit uses a Proxy object for the state and does a comparison to see if it's modified.
Things I tried:
state = initialState;
and
state = { ...initialState };
Didn't work. The store works, and holds data, other actions work. How do I proceed?
EDIT: Further debugging revealed that my own reducer was called BEFORE the redux-persist reducer, and redux-logger reported that my reducer did not change the state at all.

I'm facing a similar issue (not re-rendering) and came by this thread today:
Seems like you can't replace state objects entirely.
From: https://redux-toolkit.js.org/usage/immer-reducers
Sometimes you may want to replace the
entire existing state, either because you've loaded some new data, or
you want to reset the state back to its initial value.
WARNING A common mistake is to try assigning state = someValue
directly. This will not work! This only points the local state
variable to a different reference. That is neither mutating the
existing state object/array in memory, nor returning an entirely new
value, so Immer does not make any actual changes.
const initialState = []
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
brokenTodosLoadedReducer(state, action) {
// ❌ ERROR: does not actually mutate or return anything new!
state = action.payload
},
fixedTodosLoadedReducer(state, action) {
// ✅ CORRECT: returns a new value to replace the old one
return action.payload
},
correctResetTodosReducer(state, action) {
// ✅ CORRECT: returns a new value to replace the old one
return initialState
},
},
})
So
state = initialState;
would be
return initialState;

This turned out to be the solution:
extraReducers: {
[PURGE]: (state: UserState, action: string): UserState => ({
...state,
...initialState,
}),
},
I don't understand why, as modifying the state object should work too, according to the documentation:
To make things easier, createReducer uses immer to let you write
reducers as if they were mutating the state directly. In reality, the
reducer receives a proxy state that translates all mutations into
equivalent copy operations.

Related

Redux Toolkit useSelector state not updating even though action is called

I've been banging my head against this, hopefully getting another pair of eyes on it will help.
I'm using Redux + Redux Toolkit in a React Native App, in a pretty simple way. I can tell (through a log statement) that my action is being called and the state is getting set, but my useSelector on the state never updates. I've tried it with shallowEqual as well, but that shouldn't be needed, since Redux Toolkit uses Immer and the object shouldn't pass an equality check after updating (most of the other similar issues I researched were due to that)
Here's my main slice, followed by all the related code. Pardon the code dump, but I want to give a full picture:
export interface Metadata {
title: string
author: string
firstLines: string
id: string
}
type MetadataState = Record<string, Metadata>
export const metadataSlice = createSlice({
name: "metadata",
initialState: {} as MetadataState,
reducers: {
setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
state = action.payload
console.log("new metadata: ", state)
},
addMetadata: (state: MetadataState, action: PayloadAction<Metadata>) => {
state[action.payload.id] = action.payload
}
}
});
I have an async action to load the metadata from AsyncStorage (like LocalStorage on mobile), as follows:
export function loadMetadata() {
return async (dispatch: AppDispatch, getState: () => RootState) => {
const maybeMetadata = await AsyncStorage.getItem("metadata");
if(maybeMetadata) {
dispatch(metadataSlice.actions.setMetadata(JSON.parse(maybeMetadata)))
return true
} else {
return false
}
}
}
And I dispatch that in my main component as follows:
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(loadMetadata())
}, [])
In another component, I'm trying to access the state simply by doing:
const metadata = useAppSelector(state => state.metadata)
Any idea what's going on? The state just never seems to update, even though I see my action being called and update the state within it. Is it not being dispatched correctly? I tried directly accessing the state with store.getState() and the state seems empty, is it somehow just not being set?
I'm honestly pretty lost, any help is appreciated.
The issue had to do with how Immer (which Redux Toolkit leverages for allowing mutable operations) works.
setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
state = action.payload
console.log("new metadata: ", state)
}
Instead of mutating state, I reassigned it, which messed up the way Immer keep track of draft states. The console.log statement returned the new state, but it didn't work with Immer. Instead, I needed to do this:
setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
// simply return the new state, since I'm changing the whole state
return action.payload
}
And it works fine now. I'm kind of surprised I didn't see this documented (it may be somewhere) or get some sort of warning, but good to know for the future!
An addition to Nathan's answer, to avoid linters flooding your code and for proper readability, instead of:
setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
return action.payload
}
Do it like this:
setMetadata: (state: MetadataState, action: PayloadAction<MetadataState>) => {
return {...state, ...action.payload}
}
By so doing, first parameter of the action state, is put to use as it should

Updating getter value Vuex store when state changes

I'm trying to figure out how to properly update a getter value when some other variable from VueX changes/updates.
Currently I'm using this way in a component to update:
watch: {
dates () {
this.$set(this.linedata[0].chartOptions.xAxis,"categories",this.dates)
}
}
So my getter linedata should be updated with dates value whenever dates changes. dates is state variable from VueX store.
The thing is with this method the value won't be properly updated when I changed route/go to different components. So I think it's better to do this kind of thing using the VueX store.
dates is updated with an API call, so I use an action to update it.
So the question is how can I do such an update from the VueX store?
EDIT:
I tried moving this to VueX:
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit("SET_DATA", {
this.linedata[0].chartOptions.xAxis,"categories": response.data.dates1,
this.linedata[1].chartOptions.xAxis,"categories": response.data.dates2
});
}
SET_DATA(state, payload) {
state = Object.assign(state, payload);
}
But the above does not work, as I cannot set nested object in action this way...
Getters are generally for getting, not setting. They are like computed for Vuex, which return calculated data. They update automatically when reactive contents change. So it's probably best to rethink the design so that only state needs to be updated. Either way, Vuex should be updated only with actions/mutations
Given your example and the info from all your comments, using linedata as state, your action and mutation would look something like this:
actions: {
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit('SET_DATA', response.data.dates);
}
}
mutations: {
SET_DATA(state, dates) {
Vue.set(state.linedata[0].chartOptions.xAxis, 'categories', dates[0]);
Vue.set(state.linedata[1].chartOptions.xAxis, 'categories', dates[1]);
}
}
Which you could call, in the component for example, like:
this.$store.dispatch('loadData');
Using Vue.set is necessary for change detection in this case and requires the following import:
import Vue from 'vue';
Theoretically, there should be a better way to design your backend API so that you can just set state.linedata = payload in the mutation, but this will work with what you have.
Here is a simple example of a Vuex store for an user.
export const state = () => ({
user: {}
})
export const mutations = {
set(state, user) {
state.user = user
},
unset(state) {
state.user = {}
},
patch(state, user) {
state.user = Object.assign({}, state.user, user)
}
}
export const actions = {
async set({ commit }) {
// TODO: Get user...
commit('set', user)
},
unset({ commit }) {
commit('unset')
},
patch({ commit }, user) {
commit('patch', user)
}
}
export const getters = {
get(state) {
return state.user
}
}
If you want to set the user data, you can call await this.$store.dispatch('user/set') in any Vue instance. For patching the data you could call this.$store.dispatch('user/patch', newUserData).
The getter is then reactively updated in any Vue instance where it is mapped. You should use the function mapGetters from Vuex in the computed properties. Here is an example.
...
computed: {
...mapGetters({
user: 'user/get'
})
}
...
The three dots ... before the function call is destructuring assignment, which will map all the properties that will the function return in an object to computed properties. Those will then be reactively updated whenever you call dispatch on the user store.
Take a look at Vuex documentation for a more in depth explanation.

Use getter in the same module in which it was created

Is it possible to initialize state's property using getter which was created in the same module? Something like this:
export const gamesModule = {
state: {
games: [],
selectedGameID: null,
playerOnTurnID: this.getters.getSelectedGame.playerData[0]
},
getters: {
getGames: state => state.games,
getselectedGameID: state => state.selectedGameID,
getSelectedGame: state => getSelectedGameById(state.games, state.selectedGameID),
},
mutations: {
SET_GAMES (state, game) {
state.games.push(game);
},
SET_SELECTED_GAME_ID (state, id) {
state.selectedGameID = id;
},
SET_PLAYER_ON_TURN_ID (state, playerID) {
state.playerOnTurnID = playerID;
}
},
actions: {
async createGame({ commit }) {
try {
const { data } = await gameService.createGame();
commit('SET_GAMES', data);
} catch (error) {
console.warn('Error creating new game: ', error);
}
},
setSelectedGameID({ commit }, id) {
commit('SET_SELECTED_GAME_ID', id);
},
};
Written like this, it does not work because getters are undefined.
this does not exist in an object's context, and is only really applicable in constructor functions or classes.
I see two problems here.
First of all, you can't reference the object itself, because it hasn't been defined yet. You would have to create a local variable before declaring the object that would have the common property, in this case, the getter function.
Second of all, more importantly, I'm not sure it would help to access the getter (Reducer) function, as it has no knowledge of the state, which is passed to it as the first parameter by the underlying Vuex library when processing mutations (Actions).
Vuex is based upon the Redux pattern, Action -> Reducer -> Store, I would recommend reading it a quick introduction on how Redux works, as it will help you understand a lot better the action flow inside of Vuex.

Vuex State Updated only in local scope of mutation

I am trying to update the store state using mutation. But the problem is that the state remains same after mutation and is updated only in scope of that mutation. It acting more of like passing arguments to a function.
The expectations are the state in the store should be updated but it's not happening. The store state is not mutated at all but the argument state is being mutated.
Here is the sample code of my implementation. I am using modules for Vuex Store and this is from
user.store.js
export const user = {
state: {
user: {},
isAuthenticated: false
},
mutations: {
updateUser(state, payload) {
Vue.set(state.user, payload.key, payload.value);
}
}
}
Edit 2:
I got to the fix of the issue and it is due to similar naming for the user store module and the state.user object.
After i changed the name of the store module, the issue was resolved.
Thank You for you contributions.
Try to use Vue.set in your mutation.
For example:
state: {
isInteractionEnabled: false
},
mutations: {
SWITCH_INTERACTION: (state, {status}) => Vue.set(state, 'isInteractionEnabled', status)
},

React-Redux, how to subscribe to changes in state and apply actions

I am building a grid and a filters component using react, redux and material-ui
Here is how my filters state object in redux looks like
{
1: {id:1, name:'firstName', value:'John'}
2: {id:2, name:'LastName', value:'Doe'}
}
Items object in the store
{
12: {id:12, firstName:'John', lastName:'Doe', contact:''}
13: {id:13, firstName:'Mark', lastName:'Doyle', contact:''}
}
when ever the state of filters object changes in the store, I want execute applyFilters function to narrowdown items. What is the best pattern for subscribing to changes in a state object and executing an action to update the state of another object in a store? or is there a better way to handle this?
Current Implementation
I am not sure if this an anti-pattern to access state in action creator.
export function updateFilters(namespace, filter, filterText) {
return (dispatch, getState) => {
// To update filters object in the store
dispatch({
type: `${namespace}/${UPDATE_FILTERS}`,
value: filterText,
payload: filter.get('id')
})
//Actual filtering
let state = getState();
let filters = state[`${namespace}`].filters;
let items = state[`${namespace}`].items;
let filteredItems = applyFilters(items, filters);
// To update filteredItems in the store
dispatch({
type: `${namespace}/${APPLY_FILTERS}`,
payload: filteredItems
})
}
}
Updated answer provided your feedback and added context. You can dispatch the second action from the reducer passing in your filter object as a parameter.
for example:
function reducer1(state = initialState, action) {
switch (action.type) {
case UPDATE_ITEMS:
+//dispatch 2nd action and pass filtered object as parameter
+//the 2nd action can update the item object
-//update items object
default:
return state
}
}
There is a lot of back and forth here, so I'm going to detail an example of how I would do this given that you want to populate your store once and work on an in-memory object afterwards.
Event Handler:
handleFilterChange = (filterText) => {
this.props.updateFilter(filterText);
}
Action:
export function updateItem(filterText) {
// you could dispatch another action, but not sure why you would
return {
type: UPDATE_FILTER, filterText,
};
}
Reducer:
function updateFilterAndItem(state = initialState, action) {
switch (action.type) {
case UPDATE_FILTER:
{
const newItems = state.items.filter(text => state.items.includes(action.filterText));
return Object.assign(
{},
state,
{
items: newItems,
filter: action.filterText
});
}
}
}
This is as close an answer as I can give you based on what you're trying to do, but I would ask you to rethink this. Why keep filter text in a redux store at all? Every time the event handler is called, the entire text string will be passed down the stack anyway. I want to answer your question as best I can to the guidelines of StackOverflow, but I also want to challenge you to think about your implementation, and if there might be a simple way to achieve your desired results.