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
Related
I'm trying to figure out how to use Redux in my React Native application.
I use Realm as a local database, and I'm pretty confused about how to implement it.
In Realm, I save an array of custom objects, so when the app starts I want to fetch the array and set it in the state of the app.
As far as I understand, I need to have an action, which looks like this:
export const fetchItems = () => {
return (dispatch) => {
dispatch({
type: "FETCH_ITEMS",
})
}
}
And have a reducer that looks kinda like this:
const initialState = {
items: [],
}
const reducer = (state = initialState, action: AnyAction) => {
switch (action.type) {
case ActionType.fetchItems:
return {
...state,
items: action.payload
}
break;
default:
return state;
}
}
But I'm not really sure how should I use this in my Home Screen for example.
I guess it should be something like this:
const { items } = useSelector(state => store.getState().items)
But then, when I'm adding a new item to the database, I should of course update the database, so when I should update the state? I tried to read articles, and watch some tutorials, but everyone works a little different and it is even more confusing.
So far, is what I wrote about Redux is right? should be using it like this?
Im using VueJS and Vuex. I have the userid into the store, this way:
vuex screenshot
And i try pass the userid to a fetch, but vuejs return error
([Vue warn]: Error in created hook: "TypeError: this.$store is
undefined")
import { LOAD_APPOINTMENTS } from './types'
export default {
loadProducts ({ commit }) {
var user = this.$store.state.user.userid
fetch('api/appointments/' + user)
.then((result) => {
return result.json()
})
.then((appointments) => {
commit(LOAD_APPOINTMENTS, appointments)
})
.catch(er => {
console.log(er)
})
}
}
First, when referencing the store within vuex files:
context.state instead of this.$store.state.
context for all of the this.$store. So, context.commit and context.dispatch.
Second, the loadProducts needs to be rewritten as an action per docs.
Third, loadProducts needs to incorporate the context as a parameter:
actions: {
loadProducts (context) {
...
context.commit(...)
...
}
}
As #phil has mentioned in this thread, it is important to view the documentation entirely, as this single answer will get you on the way to debugging the problem, but there might be multiple more problems showing up (e.g. fetch errors, file structure errors, component/App level errors).
I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);
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.
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.