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.
Related
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
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?
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.
im building an react native with redux and Firebase Realtime Database, and I'm concerned about where to subscribe to fetch my items on a screen.
Im using useEffect to dispatch the subscription to firebase db:
useEffect(() => {
dispatch(userActions.fetchPets());
}, []);
and inside the action
export const fetchPets = () => {
return async dispatch => {
const user = await firebase.auth().currentUser;
firebase
.database()
.ref(`pets/${user.uid}`)
.on("child_added", snapshot => {
const pet = snapshot.val() || null;
dispatch({ type: ADD_PET, payload: pet });
});
};
};
My problem is when my screen re-render this action executes again filling with repeated data.
This is my reducer:
case ADD_PET:
return {
...state,
pets: [...state.pets, action.payload]
};
My question
Should I filter my state with key to delete repeated?
Should I put my subscription in another place? like a middleware or something? there is a pattern for this?
PS: "Sorry by my English"
the pattern you are using is fine.
If inside payload you have an array with new elements you have, your approach works fine. But, im assuming you are getting the same elements, just with any updated property. So, for example, if you have your pets store like this:
pets: [{id: 1, name: 'whatever'}], and your payload is : [{id: 1, name: 'whatever2'}], now you have both concatenated in your store, what is bad, because is the same object, updated.
So, if you will have the full list updated in the request, i would just change your reducer to this:
const initialState = { pets: [] };
case ADD_PET:
return {
...state,
pets: action.payload
};
So everytime you make the api request, you will have the updated list of elements.
Another case is if you get in the request only the updated, and you will have to filter your object based on ids, and then just replace the updated ones. But i dont think it is your case.
I'm trying to wait for certain strings in a sort of dictionary containing all the text for buttons, sections, labels etc.
I start out by sending a list of default strings to a controller that registers all the strings with my CMS in case those specific values do not already exist. After that I return a new object containing my "dictionaries", but with the correct values for the current language.
I run the call with an event listener that triggers a dispatch() on window.onload, and then add the data to a Vuex module state. I then add it to a computed prop.
computed: {
cartDictionary() {
return this.$store.state.dictionaries.myDictionaries['cart']
}
}
So now here's the problem: In my template i try to get the values from the cartDictionaryprop, which is an array.
<h2 class="checkout-section__header" v-html="cartDictionary['Cart.Heading']"></h2>
But when the component renders, the prop doesn't yet have a value since it's waiting for the AJAX call to finish. And so of course I get a cannot read property of undefined error.
Any ideas on how to work around this? I would like to have the dictionaries accessible through a global object instead of passing everything down through props since it's built using atomic design and it would be insanely tedious.
EDIT:
Adding more code for clarification.
My module:
const dictionaryModule = {
namespaced: true,
state: {
dictionaries: []
},
mutations: {
setDictionaries (state, payload) {
state.dictionaries = payload
}
},
actions: {
getDictionaries ({commit}) {
return new Promise((resolve, reject) => {
Dictionaries.init().then(response => {
commit('setDictionaries', response)
resolve(response)
})
})
}
}
}
My Store:
const store = new Vuex.Store({
modules: {
cart: cartModule,
search: searchModule,
checkout: checkoutModule,
filter: filterModule,
product: productModule,
dictionaries: dictionaryModule
}
})
window.addEventListener('load', function () {
store.dispatch('dictionaries/getDictionaries')
})
I think you can watch cartDictionary and set another data variable.
like this
<h2 class="checkout-section__header" v-html="cartHeading"></h2>
data () {
return {
cartHeading: ''
}
},
watch: {
'cartDictionary': function (after, before) {
if (after) {
this.cartHeading = after
}
}
}
Because this.$store.state.dictionaries.myDictionarie is undefined at the the begining, vuejs can't map myDictionarie['core']. That's why your code is not working.
You can do this also
state: {
dictionaries: {
myDictionaries: {}
}
}
and set the dictionaries key values during resolve.
I also would have liked to see some more of your code, but as i can't comment your questions (you need rep > 50), here it goes...
I have two general suggestions:
Did you setup your action correctly? Mutations are always synchronous while actions allow for asynchronous operations. So, if you http client returns a promise (axios does, for example), you should await the result in your action before calling the respective mutation. See this chapter in the official vuex-docs: https://vuex.vuejs.org/guide/actions.html
You shouldn't be using something like window.onload but use the hooks provided by Vue.js instead. Check this: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
EDIT: As a third suggestion: Check, whether action and mutation are called properly. If they are handled in their own module, you have to register the module to the state.