Reputation: 591
I can't seem to figure out what the issue is. I have a component called DisplayItems
that gets some data from an API (a list of items).
I am using Redux actions and thunk middleware to get the items and data from the API. I save the items and other data to the redux store as an array of objects, and I name it entriesData
. The actual array is in entriesData.data
. I am using connect to connect the state and actions to DisplayItems
' props.
Next, I have a child component called GalleryItemContentV1
. GalleryItemContentV1
isn't connected to redux. It just receives the data from DisplayItems
which loops through the array of items and passes each one to GalleryItemContentV1
. So, again, DisplayItems
loops through this.props.entriesData.data
and passes each item to GalleryItemContentV1
as a prop called entry
.
Finally, I have a child component in GalleryItemContentV1
called TestCom
. That's where the issue is. I am also passing the item that I got from DisplyItem
to TestCom
. So, the data flow is DisplayItems
(connected) > GalleryItemContentV1
(not connected) > TestCom
(connected).
TestCom
is connected to redux. It uses some actions from redux to update an item in entriesData.data
. When I update this.props.entriesData.data
in TestCom
, GalleryItemContentV1
receives the updated data just fine, but TestCom
doesn't receive anything. The item in TestCom
is never updated.
I figured out that the issue is when I connect TestCom
to redux. If TestCom
is not connected to redux, it also receives the updated props, just like GalleryItemContentV1
.
The components are below. I've tried to simplify them as much as possible.
DisplayItems
import React, {Component} from 'react';
import GalleryItemContentV1 from './GalleryItemContentV1';
import { connect } from 'react-redux';
import {getContestEntriesPageData} from '../../actions/contest';
class DisplayItems extends Component {
componentDidMount(){
this.props.getContestEntriesPageData(uri, search);
}
render(){
console.log('DisplayItems props', this.props);
return (
<div>
<div className="card-deck thumbnails" id="card-list">
{ this.props.entriesData.data instanceof Array ?
this.props.entriesData.data.map(function(object, i){
return <GalleryItemContentV1
entry={object} key={i}
arr_key={i}
/>;
}, this) : ''
}
</div>
</div>
)
}
}
const mapStateToProps = state => (
{
entriesData: state.entriesData,
}
)
const mapDispatchToProps = (dispatch) => {
return {
getContestEntriesPageData: (uri, search) => {
dispatch(getContestEntriesPageData(uri, search));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DisplayItems);
GalleryItemContentV1
import React, { Component } from 'react';
import TestCom from './TestCom';
class GalleryItemContentV1 extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<TestCom entry={this.props.entry} />
</div>
);
}
}
export default GalleryItemContentV1;
TestCom
I am outputting the results of this.props.entry.VotesCount
(the item from DisplayItems
) in TestCom
. That's where the issue occurs. For some reason, when I connect TestCom
to redux, its props aren't updated, but when it's disconnected from Redux, then its props are updated.
import React, {Component} from 'react';
import { connect } from 'react-redux';
class TestCom extends Component {
constructor(props) {
super(props);
}
render(){
console.log('TestCom props', this.props);
return (
<div>{this.props.entry.VotesCount}</div>
)
}
}
const mapStateToProps = state => (
{
user: state.user
}
)
export default connect(mapStateToProps)(TestCom);
Here's where I'm configuring redux, but I don't think it's relevant to know since GalleryItemContentV1
is seeing the updated data just fine.
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import ContestReducer from '../reducers/contest';
export default function configureStore(initialState) {
return createStore(
ContestReducer,
initialState,
applyMiddleware(thunk)
);
}
So, to summarize.
DisplayItems (is connected, receives updated data from store okay) > GalleryItemContentV1 (is not connected, receives updates okay) > TestCom (is connected, does not receive updates, but receives updates if disconnected)
Here's my reducer, just in case it helps.
import * as ContestActionTypes from '../actiontypes/contest';
const initialState = {
contest: null,
subscriptions: [],
user: null,
page: null
}
export default function Contest(state=initialState, action) {
console.log('redux action reducer: ', action)
switch(action.type) {
case ContestActionTypes.HANDLE_VOTE:
const newState = { ...state };
if (newState.entriesData) {
const index = newState.entriesData.data.findIndex(x => x.id === action.entry.id)
newState.entriesData.data[index].VotesCount = action.vote;
} else {
newState.entry.VotesCount = action.vote;
}
return newState
default:
return state;
}
}
I also noticed the same thing happening with GalleryItemContentV1
. If I connect it, then it stops receiving updates from DisplayItems/the redux store.
Any input is appreciated. Thanks!
Upvotes: 2
Views: 2497
Reputation: 591
markerikson was right, but I am posting the specific answer that helped me, here.
The problem was that I was trying to update an array inside of an object (entriesData being the object and entriesData.data being the array), using the normal method of Object.assign({}, state, {}) when I should have been using JSON.parse(JSON.stringify(state)). I had to read Mozilla's explanation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to understand what was going on. I saw the JSON.parse solution before, but I didn't really understand shallow and deep cloning, and, whatever shallow cloning was, I didn't think I was guilty of it. Turns out, I was.
Here's my updated reducer that works perfectly.
case ContestActionTypes.HANDLE_VOTE:
const newState = JSON.parse(JSON.stringify(state));
if (newState.entriesData) {
const index = newState.entriesData.data.findIndex(x => x.id === action.entry.id)
newState.entriesData.data[index].VotesCount = action.vote;
} else {
newState.entry.VotesCount = action.vote;
}
return newState;
Upvotes: 0
Reputation:
Yes, I also think you are mutating your state. The following is the TOGGLE_TODO
case from reducer. I hope this code snippet will work for your code.
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
So, in your case if id matches then update is with action.vote
. Also read about deep and shallow object copies.
Upvotes: 0
Reputation: 67539
You're mutating your state. You're making a shallow copy of the top-level state in that slice reducer, but you aren't making copies of all the nested levels inside. Then, your connected component is extracting state.entriesData
, which hasn't changed by reference. So, the connect
wrapper component thinks that the state hasn't changed at all, and doesn't re-render your own component.
The Redux docs page on "Immutable Update Patterns" specifically points this out as a common mistake.
If updating nested state immutably is too much of a pain, I'd suggest looking into the immer
immutable update library. Also, our new redux-starter-kit
package uses Immer internally to let you write simpler reducers.
Also, see my post Idiomatic Redux: The History and Implementation of React-Redux for background and details on how connect
works internally.
Upvotes: 4