I'm kind of new to React.js & Redux, so I have encountered a problem with Reducers.
I am creating a site that have a main "Articles" page, "Question & Answers" page, I created for each one a separate Reducer that both work just fine.
The problem is in "Main Page" which contains a lot of small different pieces of information, and I don't want to create each little different piece of information its on Reducer, so I am trying to create one Reducer which will handle a lot of very small different pieces of information, and I can't make that work, inside the main "Content" object, I put 2 Key Value Pairs that each have an array, one for each different information, one is "Features" info, and one for the "Header" info.
This is the error that I'm getting:
Uncaught TypeError: Cannot read property 'headerContent' of undefined
at push../src/reducers/ContentReducer.js.__webpack_exports__.default (ContentReducer.js:15)
I am not sure what's the problem, maybe my code is wrong or maybe my use of the spread operator, any solution?
I have added the necessary pages from my code:
ACTIONS FILE
export const addFeatureAction = (
{
title = 'Default feature title',
feature = 'Default feature',
} = {}) => ({
type: 'ADD_FEATURE',
features: {
id: uuid(),
title,
feature
}
})
export const addHeaderAction = (
{
title = 'Default header title',
head = 'Default header',
} = {}) => ({
type: 'ADD_HEADER',
header: {
id: uuid(),
title,
head
}
})
REDUCER FILE:
const defaultContentReducer = {
content: {
featuresContent: [],
headerContent: [],
}
}
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return [
...state.content.featuresContent,
action.features
]
case 'ADD_HEADER':
return [
...state.content.headerContent,
action.header
]
default:
return state
}
}
STORE FILE:
export default () => {
const store = createStore(
combineReducers({
articles: ArticleReducer,
qnaList: QnaReducer,
content: ContentReducer
})
);
return store;
}
The reducer function is supposed to return the next state of your application, but you are doing a few things wrong here, you are returning an array, a piece of the state and not the state object, I would suggest you look into immer to prevent this sort of errors.
Simple fix:
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return {...state, content: {...state.content. featuresContent: [...action.features, ...state.content.featuresContent]}}
// More actions are handled here
default:
return state
}
}
If you use immer, you should have something like this
export default (state = defaultContentReducer, action) => {
const nextState = produce(state, draftState => {
switch(action.type) {
case 'ADD_FEATURE':
draftState.content.featuresContent = [...draftState.content.featuresContent, ...action.features]
});
break;
default:
break;
return nextState
}
Related
I have the following code (I deleted most of it to make it easier to understand - but everything works):
"role" reducer:
// some async thunks
const rolesSlice = createSlice({
name: "role",
initialState,
reducers: { // some reducers here },
extraReducers: {
// a bunch of extraReducers
[deleteRole.fulfilled]: (state, { payload }) => {
state.roles = state.roles.filter((role) => role._id !== payload.id);
state.loading = false;
state.hasErrors = false;
},
},
});
export const rolesSelector = (state) => state.roles;
export default rolesSlice.reducer;
"scene" reducer:
// some async thunks
const scenesSlice = createSlice({
name: "scene",
initialState,
reducers: {},
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => {
state.scenes = payload.map((scene) => scene);
state.loading = false;
state.scenesFetched = true;
}
}
export const scenesSelector = (state) => state.scenes;
export default scenesSlice.reducer;
a component with a button and a handleDelete function:
// a react functional component
function handleDelete(role) {
// some confirmation code
dispatch(deleteRole(role._id));
}
My scene model (and store state) looks like this:
[
{
number: 1,
roles: [role1, role2]
},
{
number: 2,
roles: [role3, role5]
}
]
What I am trying to achieve is, when a role gets deleted, the state.scenes gets updated (map over the scenes and filter out every occurrence of the deleted role).
So my question is, how can I update the state without calling two different actions from my component (which is the recommended way for that?)
Thanks in advance!
You can use the extraReducers property of createSlice to respond to actions which are defined in another slice, or which are defined outside of the slice as they are here.
You want to iterate over every scene and remove the deleted role from the roles array. If you simply replace every single array with its filtered version that's the easiest to write. But it will cause unnecessary re-renders because some of the arrays that you are replacing are unchanged. Instead we can use .findIndex() and .splice(), similar to this example.
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => { ... }
[deleteRole.fulfilled]: (state, { payload }) => {
state.scenes.forEach( scene => {
// find the index of the deleted role id in an array of ids
const i = scene.roles.findIndex( id => id === payload.id );
// if the array contains the deleted role
if ( i !== -1 ) {
// remove one element starting from that position
scene.roles.splice( i, 1 )
}
})
}
}
I'm using redux and my reducer function is called in every time the dispatch called but the state is not updating. and there is no difference between the first state and the next state.
ArtclesReducer.ts
const defaultState: Articles = {
articles: [{token: "teken", title: "text", featureImageUrl: ""}],
}
export const articlesReducer: Reducer<Articles, any> = (state = defaultState, action: ArticlesActionTypes) => {
let nextState: Articles = {
articles: state.articles,
}
switch (action.type) {
case ADD_ARTICLES :
let allArticles = [...state.articles, ...action.payload]
return {
articles: [{title: "", token: "", featureImageUrl: ""}, {
title: "",
token: "",
featureImageUrl: ""
}, {title: "", token: "", featureImageUrl: ""}, {title: "", token: "", featureImageUrl: ""}]
}
case UPDATE_ARTICLE:
console.log("in update article")
for (let i = 0; i < nextState.articles.length; i++) {
if (nextState.articles[i].token == action.payload.token) {
nextState.articles[i] = action.payload;
break;
}
}
break;
case DELETE_ARTICLE:
console.log("in delete article")
nextState.articles = nextState.articles.filter(value => {
return value.token != action.payload;
})
break;
default:
}
return nextState;
}
as shown up I return a non-empty state.
as you see the state it becomes the same and not updating
Redux Toolkit
If you are unsure about how to update the state without mutating it, you can save yourself a lot of frustration by using Redux Toolkit. The toolkit makes it so you can write the code as if you were mutating the state (it handles the immutability issue behind the scenes).
Here's how this reducer would look with the createReducer utility:
const articlesReducer = createReducer(defaultState, {
[ADD_ARTICLES]: (state, action) => {
// We don't return anything. We just mutate the passed-in draft state.
state.articles.push(action.payload);
},
[UPDATE_ARTICLE]: (state, action) => {
// Find which article we are updating
const index = state.articles.findIndex(
article => article.token === action.payload.token
);
// Replace that index with the new article from the payload
state.articles[index] = action.payload;
},
[DELETE_ARTICLE]: (state, action) => {
// We replace the articles array with a filtered version
state.articles = state.articles.filter(
article => article.token === action.payload
);
}
});
Most people don't use createReducer directly because there is an even better utility createSlice that creates the action names and action creator functions for you!
Vanilla Redux
Of course you can still do this the "old-fashioned" way. But you need to be sure that you never mutate any part of the state and that every case returns a complete state.
nextState.articles[i] = action.payload is actually a mutation even though nextState is a copy because it is a shallow copy so the articles property points to the same array as the current state.
I do not recommend this approach unless you are confident that you know what you are doing, but I want to include a correct version to show you how it is done.
export const articlesReducer: Reducer<Articles, any> = (state = defaultState, action: ArticlesActionTypes) => {
switch (action.type) {
case ADD_ARTICLES:
return {
...state,
articles: [...state.articles, ...action.payload]
};
case UPDATE_ARTICLE:
return {
...state,
articles: state.articles.map((article) =>
article.token === action.payload.token ? action.payload : article
)
};
case DELETE_ARTICLE:
return {
...state,
articles: state.articles.filter((article) =>
article.token !== action.payload
)
};
default:
return state;
}
};
Note: Writing ...state like you see in most examples is technically not necessary here since articles is the only property in your state so the there are no other properties to be copied by ...state. But it might be a good idea to include it anyways in case you want to add additional properties in the future.
I have array in redux. I am showing datas on Flatlist. However, When I edited array data , flatlist not re-render. How can i solve this problem? I checked my redux and is working fine
this.props.notes[this.state.Index]={
color: JSON.stringify(BgColor),
date: this.state.fullDate.toString(),
note: this.state.noteText,
name: this.state.noteName,
type: "note",
noteID:this.props.notes[this.state.Index].noteID
}
this.props.editNotes(this.props.notes);
Flatlist code;
<FlatList
ref={(list) => this.myFlatList = list}
data={this.props.notes}
showsVerticalScrollIndicator={false}
renderItem={({item, index})=>(
)}
removeClippedSubviews={true}
extraData={this.props.notes}
/>
mapStateToProps on same page with Flatlist
const mapStateToProps = (state) => {
const { notes } = state
return { notes }
};
Reducer
const notes = [];
const notesReducer = (state = notes, action) => {
switch (action.type) {
case 'editNotes':
return state = action.payload;
default:
return state
}
};
export default notesReducer;
The reason it's not updating is because you're not returning a new array. The reference is same.
Return the updated state like return [...state,action.payload]
The reason it's not updating the data correctly is because the mutation.
The problematic code is this part.
this.props.notes[this.state.Index]={
color: JSON.stringify(BgColor),
date: this.state.fullDate.toString(),
note: this.state.noteText,
name: this.state.noteName,
type: "note",
noteID:this.props.notes[this.state.Index].noteID
}
this.props.editNotes(this.props.notes);
It should be in this way
const { notes, editNotes } = this.props;
const newNotes = [...notes];
const { index } = this.state;
newNotes[index] = {
//update data
}
editNotes(newNotes);
You can fix the issue in many ways but the wrong part I see in your code is Reducer. As per the standard, your reducer should be a Pure Function and the state should not mutate.
const notes = [];
const notesReducer = (state = notes, action) => {
switch (action.type) {
case 'editNotes':
return {
...state,
...action.payload;
},
default:
return state
}
};
export default notesReducer;
This should resolve your issue.
Suggestion:
Try to create a nested hierarchy in redux like
const initialState = {
notes: [],
};
const notesReducer = (state = initialState, action) => {
switch (action.type) {
case 'editNotes':
return {
...state,
notes: [
...state.notes,
...action.payload.notes,
],
},
default:
return state
}
};
export default notesReducer;
I am trying to make a todo app using redux and I'm stack on how to delete a todo from the array.
reducer.js
export default function todo(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
case 'REMOVE_TODO':
return {
id: action.id,
...state.slice(id, 1)
}
default:
return state;
}
}
action.js
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
export const removeTodo = id => {
type: 'REMOVE_TODO',
id
}
So far i can add and toggle a todo as completed or not. Thanks
Using redux you need to return all array elements except the removed one from reducer.
Personally, I prefer using the filter method of the Array. It'll return you a shallow copy of state array that matches particular condition.
case 'REMOVE_TODO':
return state.filter(({id}) => id !== action.id);
In react redux application, you should know, you always have to create a new object,
to deleting an item please use spread operator like this :
return [...state.filter(a=>a.id !== id)]
I'm new to Redux so please bear with me. I am wondering if something like the following is possible and/or optimal and if so, how do you update the nested object values in the reducer?
const initialState = {
banner: {
backgroundColor: 'black',
text: 'Some Title',
textColor: 'white',
image: null
},
nav: {
mainOpen: false,
authOpen: false,
}
...
}
And then in a reducer something like this does not seem to work...
export default function reducer(state = initialState, action = {}) {
const {type, data} = action;
switch (type) {
case SET_BANNER_TEXT:
return {
...state,
banner.text: data //<-- ****THIS FAILS WITH UNEXPECTED TOKEN!!!****
}
...
}
Or is it better to to have a 'banner' reducer, a 'nav' reducer and so on??
TIA!
I'm a bit new to redux too, so I can't speak too much to 'best practice'. I would lean towards a new reducer personally, since you have have specific actions like SET_BANNER_TEXT (color, img, etc?) that modify a specific portion of your state tree and nothing else. Making your reducers simple by breaking them apart, (even if there are a lot of them), will make things easier to trace down the road.
Syntactically you could achieve what you're trying to do with something like:
export default function reducer(state = initialState, action = {}) {
const {type, data} = action;
switch (type) {
case SET_BANNER_TEXT:
const newBannerState = Object.assign({}, state.banner, {text: data});
return Object.assign({}, state, {banner: newBannerState});
}
Since you're updating a key of an object, try using this to update the key
const state = {
...initialState,
banner: {
...initialState.banner, // extend
text: 'newText'
}
}
which translates to
var state = _extends({}, initialState, {
banner: _extends({}, initialState.banner, {
text: 'newText'
})
});
in ES5
check this jsbin
edit: as stated in the comment below, it will overwrite the whole banner object if the code is used above. You can try the other answer in this thread using the Object.assign() and clone the banner object. If you still want to use spread operators, I updated my answer.
I think it is also better to have specific reducers for deeply nested state.And I will write it something like this
export function rootReducer(state = initialState, action) {
switch (action.type) {
case SET_BANNER_TEXT:
case SET_BANNER_BG:
return Object.assign({}, state,
{
banner: bannerReducer(state.banner, action)
}
);
// ...
default:
return state;
}
}
function bannerReducer(state, action) {
switch (action.type) {
case SET_BANNER_TEXT:
return Object.assign({}, state, {text: action.payload});
case SET_BANNER_BG:
return Object.assign({}, state, {backgroundColor: action.payload});
// ...
default:
return state;
}
}