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.
Related
Say I have some initial state like
const initialState = {
post: { comments: {
{0: {id:'1',name:'myname',statusdata: false}},
{1: {id:'2',name:'yourname',statusdata: true}},
},
};
And I want add to data as the result of an action but the data I want to add is going to be an array. How do I go about this?
export default produce((draft, action) => {
switch (action.type) {
case UPDATE_NAME:
draft.posts.comments.name = action.payload;
break;
case CHANGE_STATUS:
draft.posts.comments.statusdata = !action.payload;
break;
default:
}
}, initialState);
I have this error
Error: [Immer] Immer only supports setting array indices and the 'length' property
I have made assumptions on the schema of comments and action.payload. Try the following solution.
Object.values(draft.post.comments).map((comment){
if(comment.id == action.payload.id){
const updatedComment = Object.extend({}, comment, {"name": action.payload.name});
return updatedComment;
}
return comment;
})
I'm try
export default produce((draft, action) => {
switch (action.type) {
case CHANGE_STATUS:
draft.posts.comments[
draft.posts.comments.findIndex(i => i.id === action.payload)
].statusdata = action.payload;
return draft;
default:
}
}, initialState);
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
}
I'm working on some legacy code that used to do full deep cloning of the redux state in a react application. Here's the original boilerplate, which is used as a basis for the reducers used in the application:
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = _.cloneDeep(state)
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = {
...nextState,
data: _.get(action, 'data.storeData', action.data),
isFetching: false,
isInited: true,
}
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState.isFetching = false
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action, _.cloneDeep(state))
}
return nextState
}
Those clone deeps are all big no-nos, so I'm trying to leverage immer's produce function to mutate drafted copies of the state before returning the new state.
Problem is, I've been unable to get everything in sync. Some pieces of state won't update correctly here or there. Here's my attempt at refactor so far:
import produce from 'immer'
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
return produce(state, (draft) => {
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
draft.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
draft.data = _.get(action, 'data.storeData', action.data)
draft.isFetching = false
draft.isInited = true
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
draft.isFetching = false
}
return draft
})
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](draft, action, state)
}
return draft
})
}
I tried to unfreeze the touched objects, but still no dice. Is my implementation just off? Or am I misunderstanding how produce works?
Hell, do I ever need something like immer if I'm just trying to get those two lodash cloneDeep calls out of there?
EDIT: Here's an example of a custom handler that would invoke the final line:
LOCATION_CHANGE: (state, action) => {
// bootstrap
if (_.isUndefined(state.location)) {
state.location = action.location
}
state.next = {
location: action.location,
changed: action.changed,
}
state.isNavigating = true
return state
},
VIEW_ROUTE_MATCH: (state, action) => {
state.next = {
...state.next,
match: action.match,
view: action.view,
}
return state
},
I'd say you don't need Immer, in this instance, as you're not really making deep or complex changes, but it can still help tidy up parts. Immer's going to force three lines of code, so I stick with the spread operator if that'll keep it to one line.
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = state;
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState = { ...nextState, isFetching: true };
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = produce(state, draft => {
draft.data = _.get(action, 'data.storeData', action.data);
draft.isFetching = false;
draft.isInited = true;
});
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState = { ...nextState, isFetching: false };
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action);
}
return nextState
}
const handlers = {
LOCATION_CHANGE: (state, action) => {
return produce(state, draft => {
if (_.isUndefined(state.location)) {
draft.location = action.location;
}
draft.next = {
location: action.location,
changed: action.changed,
}
draft.isNavigating = true
});
},
VIEW_ROUTE_MATCH: (state, action) => {
return produce(state, draft => {
draft.match = action.match;
draft.view = action.view;
});
},
}
The first issue is that your original code really shouldn't have been doing a "deep clone" at all, and especially not at the top of the reducer. That means it was doing extra work for every dispatched action, even if it wasn't relevant, and also likely causing the UI to re-render even if the data's values hadn't really changed. So, as you said, a "big no-no". Based on that, yes, I'd say Immer is useful here.
Second, you shouldn't be using promiseActionTypes.map() conceptually, since you're trying to do some updates instead of returning a new array. Use .forEach() as the original code did.
Third, you haven't described which fields aren't being update properly, so it's a bit hard
Beyond that, the draft code looks okay. However, the handlers line looks suspicious in this case. My guess is that those additional handlers are probably doing their own immutable work, rather than attempting to "modify" the draft state value. Oh, and you aren't even passing the draft to them anyway. So, if you're expecting those to be generating the rest of the changes, you need to A) pass draft in to the handlers, and B) change them to "modify" the draft.
I am trying to simply sort on the redux store data in mapStateToProps, similar to how it is being done in Dan Abramov's Egghead.io video: https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers
My problem is, initially the state is returning undefined (as it is fetched asynchronously), so what would be the best way to deal with this? Current code is as follows (_ is the ramda library):
const diff = (a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
}
const mapStateToProps = (state) => {
return {
transactions: _.sort(diff, state.transactions.all),
expenditure: state.expenditure.all,
income: state.income.all
}
}
I thought that transactions.all should initially be an empty array (which would mean the code would work) because of the initial state set in the reducer:
const INITIAL_STATE = { transactions: { all: [] }, transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_TRANSACTION:
return { ...state, transaction: action.payload.data }
case FETCH_TRANSACTIONS:
return { ...state, all: action.payload.data }
case EDIT_TRANSACTION:
return { data: action.data }
case ADD_TRANSACTION:
return { data: action.data }
case DELETE_TRANSACTION:
return { ...state }
default:
return state
}
}
Thanks in advance.
As you said, it is fetched asynchronously. Perhaps when the component rendered, data isn't ready yet which resulted to an undefined object.
const SampleComponent = (props) => {
if(props.transaction === undefined)
return <Spinner /> // Loading state
else
// your implementation
}
You can further make the code cleaner as explained by Dan himself in the docs here: http://redux.js.org/docs/advanced/AsyncActions.html
Managed to solve this, because in combine reducers, I had set transactions with the name transactions and then in the reducer, I essentially had the initial state set to transactions: { all: [] } }.
This was causing state.transactions.all to be undefined, as the correct state structure was actually state.transactions.transactions.all.
After updating the transactions reducer to:
const INITIAL_STATE = { all: [], transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {...
The initial empty transactions array prior to the promise returning meant the sort no longer causes an error, and is then correctly sorted on load.
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;
}
}