I am learning how to implement redux from the ground up, and have run into a problem with my components' re-rendering. My search for this issue on StackOverflow has produced a gazillion results, of which the answer to the question is always "you mutated your state." I have read the connect documentation, I've looked at a bunch of people with similar problems, and I just can't see where state mutation might be the problem here, so I'm going to try asking with my simple example.
Here's my container component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addPokemon } from '../../../redux/actions';
import ReduxTester from '../../components/redux_tester/redux_tester';
export class ReduxTesting extends Component {
constructor(props) {
super(props);
}
addPokemon(name, pokeType) {
addPokemon(name, pokeType);
}
render() {
return (
<div>
<ReduxTester
pokemon={this.props.pokemon}
addPokemon={this.addPokemon}
/>
</div>
);
}
}
const MapStateToProps = function(state) {
return {
pokemon: state.pokemon,
};
};
export default connect(MapStateToProps)(ReduxTesting);
Here's my reducer:
const defaultState = {
pokemon: {},
};
export default function pokemonReducer(state = defaultState, action) {
const newState = Object.assign({}, state);
switch (action.type) {
case 'ADD_POKEMON':
newState.pokemon[action.name] = action.pokeType;
break;
default:
break;
}
return newState;
}
My specific issue is simply that ReactTesting's componentWillReceiveProps method is not firing, and so the component is not being updated and re-rendered. Note that the mapStateToProps method is firing after the action is dispatched. I know this is such a repetitive issue and it's unlikely that my problem is something different, but I just can't find the answer. Any assistance is appreciated.
Edit: For additional clarification, here is my actions.js, where I've imported the dispatch function directly:
import { dispatch } from './store';
// Returns an action that fits into the reducer
export function addPokemon(name, pokeType) {
dispatch({
type: 'ADD_POKEMON',
name,
pokeType,
});
}
Edit 2: I've found some additional information, but I don't understand it. It seems that in MapStateToProps, a re-render is triggered when I assign the entire Redux state to one prop - but not when I assign just a portion of the Redux state to prop.
This triggers a re-render:
const MapStateToProps = function(state) {
return {
pokemon: state,
};
};
This does not:
const MapStateToProps = function(state) {
return {
pokemon: state.pokemon,
};
};
Yet I have confirmed my redux store does have the pokemon property and that is where the updates are occurring in the state. Any insight?
Calling your action creator without going through redux won't dispatch the action:
export class ReduxTesting extends Component {
constructor(props) {
super(props);
}
addPokemon(name, pokeType) {
this.props.addPokemon(name, pokeType);
}
.
.
.
function mapDispatchToProps(dispatch) {
return({
addPokemon: (name, pokeType) => {dispatch(addPokemon(name, pokeType))}
})
}
export default connect(MapStateToProps, mapDispatchToProps)(ReduxTesting);
EDIT 2
You're probably mutating your state, try with something like this:
Reducer
switch (action.type) {
case 'ADD_POKEMON':
return {
...state,
pokemon[action.name]: action.pokeType;
};
Related
I am making a React-Native app in which I have a navigator from React Navigation and I also want to implement Redux. I am trying to create a global counter that updates based on an argument.
Here is the actions:
export const setFlags = (value) => {
return {
type: 'SETFLAGS',
value
}
}
export const setNonFlags = (value) => {
return {
type: 'SETNONFLAGS',
value
}
}
Here is the reducer, because its two things that have identical functionality I thought one would work (I am new to Redux):
const initialState = {
flags:0,
nonFlags:0,
}
const AllFlagReducer = (state = initialState, action) =>{
switch(action.type){
case 'SETFLAGS':
return state.flags = state.flags + action.value
case 'SETNONFLAGS':
return state.nonFlags = state.nonFlags + action.value
}
return state
}
export default AllFlagReducer
And here is the button where I would like to send the local state of the "flag" and "nonFlag" to the redux global states. After which I reset the local states and move to the next screen.
<TouchableOpacity style={styles.resetButton}
onPress= {
// dispatch something like flags(in redux):this.state.flags
// dispatch nonFlags(in redux): this.state.nonFlags
() =>{this.resetAll();
navigation.navigate('Specific Scams')
}}>
Help would be greatly appreciated.
UPDATE 1:
The entire component:
class ScamTree extends React.Component {
constructor(props){
super(props)
this.state = {
flags : 0,
nonFlags: 0,
qAnswered:0
}
}
functions that might matter:
resetAll = () =>{
this.setState({flags:0})
this.setState({nonFlags:0})
this.setState({qAnswered:0})
}
the button, (I did not make a separate component for just the button):
<TouchableOpacity style={styles.resetButton}
onPress= {
// dispatch something like flags:this.state.flags
// dispatch nonFlags: this.state.nonFlags
() =>{this.resetAll();store.dispatch({type:"SETFLAGS",value:5})
navigation.navigate('Specific Scams')
}}>
<Text style={{paddingHorizontal:40}}>NEXT</Text>
</TouchableOpacity>
the export to make React Navigation Work:
export default function(props) {
const navigation = useNavigation();
return <ScamTree {...props} navigation={navigation} />;
}
In your .js class you have to bind your action like this
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux';
<TouchableOpacity style={styles.resetButton}
onPress= {
// dispatch something like flags(in redux):this.state.flags
// dispatch nonFlags(in redux): this.state.nonFlags
() =>{
this.resetAll();
this.props.commanAction.setFlags(your value);
this.props.commanAction.setNonFlags(your value);
navigation.navigate('Specific Scams')
}}>
const mapDispatchToProps = dispatch => {
return {
commanAction: bindActionCreators(commanAction, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Your .js className);
Other code are looks good.
If I understand a comment of yours in another answer, and the snippet of your question update, I believe you need to decorate this functional component:
export default function(props) {
const navigation = useNavigation();
return <ScamTree {...props} navigation={navigation} />;
}
You want to decorate this component with the connect HOC to connect it to your redux store.
First I'd convert that function to a new withNavigation HOC:
const withNavigation = WrappedComponent => props => {
const navigation = useNavigation();
return <WrappedComponent{...props} navigation={navigation} />;
};
withNavigation.displayName = `withNavigation(${WrappedComponent.displayName || 'Component'})`;
Now you can decorate ScamTree as follows:
export default withNavigation(ScamTree);
But now you also need to connect your action creators to your redux store, decorate it with react-redux's connect HOC:
const mapDispatchToProps = {
setFlags,
setNonFlags,
};
export default withNavigation(
connect(null, mapDispatchToProps)(ScamTree),
);
Note: I see it seems you store in local state some flag values, not sure if that is related, but you can map in initial state for this with a mapStateToProps as the first parameter for connect instead of null.
By now you've probably noticed that each new HOC creates some nesting, and this will get worse with the more HOC's used. The solution is to use redux's compose HOC. That's right, it's not just for composing middleware for the store. It can compose all the decorators into a single HOC to wrap your exported component, or in other words, it flattens/eliminates the nesting.
Tips
All compose does is let you write deeply nested function
transformations without the rightward drift of the code. Don't give it
too much credit!
...
import { compose } from 'redux';
...
class ScamTree extends Component { ... }
const mapDispatchToProps = {
setFlags,
setNonFlags,
};
export default compose(
withNavigation,
connect(null, mapDispatchToProps),
)(ScamTree);
Note: Your reducers need to always return new state object references and never mutate existing state. Also, although your returns do work correctly, the reducer pattern is to return unhandled action types in the default switch case:
const AllFlagReducer = (state = initialState, action) => {
switch(action.type){
case 'SETFLAGS':
return { ...state, flags: state.flags + action.value };
case 'SETNONFLAGS':
return { ...state, nonFlags: state.nonFlags + action.value };
default:
return state;
}
}
In your reducer cases right now you writing new state over the whole state instead just in the fit place at state object, it can be written like
const AllFlagReducer = (state = initialState, action) =>{
switch(action.type){
case 'SETFLAGS':
return { ...state, flags: state.flags + action.value }
case 'SETNONFLAGS':
return { ...state, nonFlags: state.nonFlags + action.value }
default: return state
}
}
export default AllFlagReducer
I have a Comment component that has a delete button. When the button is clicked, it called the action, makes the axios call, and when the call returns it dispatches the update to the reducer. The only problem is that it's not triggering the rerender of the parent component. The action, although it is updating the state, does not appear in the list of dispatched actions in the Redux DevTools. All other actions work and display in the DevTools, but for some reason this one doesn't.
My thought after reading the comment section below is that it's because I'm making a shallow copy of my object. Am I wrong to think that making a shallow copy, modifying a deeper object, and returning the shallow copy wouldn't trigger a rerender? Isn't the fact that the shallow object is a different reference enough to trigger it? I'm confident I'm doing this the same way in other places and I havenn't have a problem elsewhere. It
This is the action list in Redux DevTools after deleting the comment. I would expect that it would have "delete_comment" near the bottom somewhere, but it's not:
The data is passed from the parent components CommentList -> CommentThread -> Comment.
Is this truly dispatching?
This is a simplified component:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {
deleteComment,
} from "../../actions/comment_actions";
class Comment extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const {comment, data} = this.props;
if (!data) {
//console.log("mir_data doesnt exist");
return <div/>;
}
return (
<div key={"comment" + comment.id} id={"c" + comment.id}>
<button onClick={() => this.props.deleteComment(comment.id)}>Delete</button>
</div>
);
}
}
function mapStateToProps(state) {
return {
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
deleteComment,
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Comment);
Here's a simplified action file:
export const DELETE_COMMENT = 'delete_comment';
export function deleteComment(id, callback = null) {
return (dispatch, getState) => {
axiosInstance.post('/delete-comment/', {comment_id: id}, getState().api.axios).then(
response => {
dispatch(dispatchDeleteComment(id));
//dispatch(dispatchViewUserInfo(response.data));
if (response.status === 200)
callback();
}
);
}
}
export function dispatchDeleteComment(id) {
return {
type: DELETE_COMMENT,
payload: id
};
}
Here's the simplified reducer:
import {DELETE_COMMENT} from "../actions/comment_actions";
export default function(state = {}, action){
let newState = {...state};
switch(action.type){
case DELETE_COMMENT:
//some logic
delete newState.comments[action.payload];
return newState;
default:
return state;
}
}
I have a React component that currently just retrieves a state from Redux. Here is the general layout:
const mapStateToProps = state => {
return { stuff: state.stuff };
};
class MyComponent extends React.Component {
// use 'stuff' from redux to build the Views
}
export default connect(mapStateToProps)(MyComponent);
But now, what if I want to add a button that changes another Redux state called other?
To save the new Redux state, I know we have to create a dispatch to the action. ie,
const mapDispatchToProps = dispatch => {
....
};
Then finally connect them:
connect(null, mapDispatchToProps)(MyComponent);
But my confusion is if I am already connecting with mapStateToProps, how can I also map it to mapDispatchToProps so that I can update the Redux state in the same component?
You can use both ;-)
For example :
Action.js
export const textChanged = (newText) => {
return { type: "TEXT_CHANGED", newText }
};
HomeScene.js :
import { textChanged } from "../actions;
...
render () {
const { myText } = this.props;
<TextInput
value={myText}
onChangeText={(newText) => this.props.textChanged(newText)}
/>
}
function mapStateToProps(state) {
return {
myText: state.appContent.myText
};
}
export default connect(mapStateToProps, { textChanged })(HomeScene);
Reducer.js
case "TEXT_CHANGED":
return {
...state,
myText: action.newText
};
Hope it helps !
Hm, looks like I asked too early. I did a bit of reading and the parameters in connect() actually accepts both.
So like this:
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
My component is straight forward:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as instanceActions from '../../../store/instances/instancesActions';
class InstanceDetailsPage extends Component {
componentWillReceiveProps(nextProps) {
console.log('will receive props');
if (nextProps.id !== this.props.id){
this.updateInstanceDetails();
}
}
updateInstanceDetails = () => {
this.props.actions.loadInstance(this.props.instance);
};
render() {
return (
<h1>Instance - {this.props.instance.name}</h1>
);
}
}
function getInstanceById(instances, instanceId) {
const instance = instances.filter(instance => instance.id === instanceId);
if (instance.length) return instance[0];
return null;
}
function mapStateToProps(state, ownProps) {
const instanceId = ownProps.match.params.id;
let instance = {id: '', name: ''};
if (instanceId && state.instances.length > 0) {
instance = getInstanceById(state.instances, instanceId) || instance;
}
return {
instance,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(instanceActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InstanceDetailsPage);
My reducer which I'm pretty sure I'm not mutating the state:
import * as types from '../actionTypes';
import initialState from '../initialState';
export default function instancesReducer(state = initialState.instances, action) {
switch (action.type){
case types.LOAD_INSTANCES_SUCCESS:
return action.instances.slice(); // I probably don't event need the .slice() here, but just to be sure.
default:
return state;
}
}
I know for sure that the state change which triggers props change because I logged this.props on the render method, and the props changed couple of times!
With that, the componentWillReceiveProps wasn't called even once.
What can cause that?
there are a few reasons why componentWillReceiveProps will not be called,
if its not receiving a new props object from the redux store
additionally these methods are not called if a component is mounted. So you may see your component update, but it is really just mounting and unmounting. to fix this, look into the parent rendering this component and check to see if it is rendering the component with a different key or if there is some sort of conditional render that may return false and cause react to unmount it.
I have redux and react working correctly I believe and I have a child component firing off an action which then makes it as a new state to the parent component's mapStatetoDispatch method. However, while I can confirm through the console log that this state is indeed new, it never hits the render method (the props come back as undefined, just as they did before when there was no props to send down to them).
I must be missing something fundamental, I even tried all the component update or next state methods. Here is my component:
https://gist.github.com/geoffreysmith/ff909437a29ae8ab8db8038276b4f3b2
Thank you!
Edit: Here is my reducer:
import { combineReducers } from 'redux';
import { routerReducer as routing } from 'react-router-redux'
import { ADD_NEW_REPORT } from '../actions/Dashboard';
function reports(state = {}, action) {
switch (action.type) {
case ADD_NEW_REPORT:
return Object.assign({}, state, { [action.index]: addReport(undefined, action) })
default:
return state
}
}
function addReport(state, action) {
switch (action.type) {
case ADD_NEW_REPORT:
return {
index: action.index,
siteAddress: action.siteAddress,
receivedAt: action.receivedAt,
status: action.status
}
default:
return state
}
}
const rootReducer = combineReducers({
reports,
routing
})
export default rootReducer
I believe mapStateToProps should return an object (the props).
So I would change it to this:
const mapStateToProps = (state) => {
return {
reports: state.reports
}
}