Search code examples
reactjsreact-nativereact-reduxreact-native-router-flux

'Warning: setState(...): Cannot update during an existing state transition' after adding react-native-router-flux Action()


I have a new React Native app - using redux, reduxThunk and react-native-router

I have a listItem component which triggers a redux action addBrandForUser() when the user clicks it.

class ListItem extends Component {

    constructor(props) {
        super(props);
        this.onAddClick = this.onAddClick.bind(this);
    }

    onAddClick() {
        let {addBrandForUser, brand} = this.props;
        addBrandForUser({userId: null, brand: brand});
    }

    render() {
        let {brand} = this.props;

        return (
            <Text style={styles.brandSelectItem}
                  onPress={this.onAddClick}>
                {brand.title}
            </Text>
        )
    }
}

The click triggers the following action:

export const addBrandForUser = ({userId, brand}) => {
    return (dispatch) => {
        dispatch({
            type: types.AddBrandToUser,
            payload: {userId, brand}
        });

        dispatch({
            type: types.SelectBrand,
            payload: brand
        });

        //Actions.main();
    }
}

Which then gets evaluated in all the correct reducers. This all works and is linked up correctly - all the debuggers hit where I'm expecting them to.

However, I'm getting the Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount exception .... but only if the Actions.main(); line is uncommented.

My router configuration is as follows:

export default class RouterComponent extends Component {
    render() {
        return (
            <Router>
                <Scene key="main" component={Main} title="Butler" hideNavBar={true} initial/>
                <Scene sceneStyle={{paddingTop: 60}} key="addBrand" component={AvailableBrands} title="Brands" hideNavBar={false}/>
            </Router>
        );
    }
}

One thing I'm noticing is that after the SelectBrand action is dispatched, the reducers update correctly and the views are updated accordingly when navigating back to the Actions.main(). However, after dispatching the AddBrandToUser action, the reducer gets the correct payload and is correctly returning the new object, but the views aren't updating accordingly.

The view which should be updated is as follows

class UserBrandList extends Component {

    renderBrands(brand) {
        return <BrandSelectItem brand={brand}/>;
    }

    render() {
        let {flex1} = sharedStyles;
        console.log('Render() ... Props.Brands: ');
        console.log(this.props.brands);

        return (
            <ScrollView style={flex1}>
                <ListView
                    enableEmptySections={true}
                    dataSource={this.props.brands}
                    renderRow={this.renderBrands.bind(this)}
                />
            </ScrollView>
        )
    }
}

const ds = new ListView.DataSource({
    rowHasChanged: (r1, r2) => r1 !== r2
});

const mapStateToProps = state => {
    console.log('MapStateToProps() .... state.userBrands: ');
    console.log(state.userBrands);
    return {
        brands: ds.cloneWithRows(state.userBrands)
    }
};

export default connect(mapStateToProps)(UserBrandList);

Below is some output from logging inside the above class. This sequence starts when the addBrandForUser() action is triggered on the listItem.

It looks like the new array is being correctly reduced, but reverts back to its former self because of the exceptions.

enter image description here

Update 1: Here is the reducer that addBrandForUser() hits.

export default (state = [], action) => {

    switch (action.type) {
        case types.RefreshUserBrands:
            return action.payload;

        case types.AddBrandToUser:
            let newArray = [...state, action.payload.brand];
            console.log('New Reduced Array: ' );
            console.log(newArray);
            return newArray;

        default:
            return state;
    }

}

Update 2: Actions is a class provided to me by react-native-router-flux:

import {Actions} from 'react-native-router-flux';
<Scene key="main" component={Main} title="Butler" hideNavBar={true} initial/>

Actions.main() from what I understand, routes to the scene with the key main

The component attached to the Main route is as follows:

export default class Main extends Component {

    constructor(props) {
        super(props);
        this.openSideMenu = this.openSideMenu.bind(this);
        this.closeSideMenu = this.closeSideMenu.bind(this);
    }

    closeSideMenu() {
        this._drawer.close()
    }

    openSideMenu() {
        this._drawer.open()
    }

    render() {
        return (
            <View style={styles.container}>

                <ButlerHeader openDrawer={this.openSideMenu}/>

                <Drawer
                    type="overlay"
                    ref={(ref) =>  {this._drawer = ref }}
                    content={<SideMenu closeDrawer={this.closeSideMenu} />}
                    openDrawerOffset={0.2}
                    drawerpanCloseMask={0.2}
                    closedDrawerOffset={-3}
                    tweenHandler={(ratio) => ({
                            main: { opacity:(2-ratio)/2 }
                        })}
                />

            </View>
        );
    }
}

Solution

  • Imagine the order doesn’t matter… and instead your Actions.main() is triggered first, then you’re attempting to change data while it’s trying to navigate. that’s likely what’s happening.