在调度之间检测到 Redux 突变

Redux mutation detected between dispatches

我在使用我目前正在处理的 react redux 时遇到了一些问题。我对 Redux 比较陌生,所以也许我在这里遗漏了一个简单的概念,但我想做的是为纸牌游戏构建一个套牌构建应用程序,我希望能够在用户添加或添加套牌时随时保存套牌从他们的牌组中移除一张牌。

但是,每当我单击“添加”或“删除”时,我都会在尝试发送更新操作时收到以下错误消息。

报错信息如下:

Uncaught Error: A state mutation was detected between dispatches, in the path `decks.0.cards.mainboard.0.quantity`. This may cause incorrect behavior.

我的容器组件

import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import DeckMobileDisplay from './DeckMobileDisplay';
import * as deckActions from '../../actions/deckActions';

export class DeckEditorContainer extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            deck: Object.assign({}, this.props.deck)
        }

        this.addCard = this.addCard.bind(this);
        this.removeCard = this.removeCard.bind(this);
    }

    addCard(board, cardName) {
        let deck = this.state.deck;
        let cards = this.state.deck.cards;

        cards[board].forEach(i => {
             if(i.name === cardName)
                i.quantity += 1;
        });

        const update = Object.assign(deck, cards);

        this.props.deckActions.updateDeck(update).then(deck => {
            console.log(deck);
        })
        .catch(err => {
            console.log(err);
        });
    }

    removeCard(board, cardName) {
        let deck = this.state.deck;
        let cards = this.state.deck.cards;

        cards[board].forEach(i => {
             if(i.name === cardName) {
                 if (i.quantity === 1) {
                     cards[board].splice(cards[board].indexOf(i), 1);
                 }
                 else {
                     i.quantity -= 1;
                 }

             }
        });

        const update = Object.assign(deck, cards);

        this.props.deckActions.updateDeck(update).then(deck => {
            console.log(deck);
        })
        .catch(err => {
            console.log(err);
        });
    }

    render() {
        const deck = Object.assign({}, this.props.deck);

        return (
            <div className="editor-container">
                <DeckMobileDisplay
                    deck={deck}
                    addCard={this.addCard}
                    removeCard={this.removeCard}
                    />
            </div>
        );
    }
}

DeckEditorContainer.PropTypes = {
    deck: PropTypes.object
};

function getDeckById(decks, id) {
    const deck = decks.filter(deck => deck.id == id);

    if (deck.length) return deck[0];
    return null;
}

function mapStateToProps(state, ownProps) {
    const deckId = ownProps.params.id;
    let deck = {
        id: '',
        userId: '',
        cards: []
    }

    if (state.decks.length > 0) {
        deck = getDeckById(state.decks, deckId);
    }

    return {
        deck: deck
    };
}

function mapDispatchToProps(dispatch) {
    return {
        deckActions: bindActionCreators(deckActions, dispatch)
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(DeckEditorContainer);

DeckMobileDisplay 组件

import React, {PropTypes} from 'react';
import TabContainer from '../common/Tabs/TabContainer';
import Tab from '../common/Tabs/Tab';
import CardSearchContainer from '../CardSearch/CardSearchContainer';
import DeckList from './DeckList.js';

class DeckMobileDisplay extends React.Component {
    render() {
        return (
            <TabContainer>
                <Tab title="DeckList">
                    <DeckList
                        deck={this.props.deck}
                        addCard={this.props.addCard}
                        removeCard={this.props.removeCard}
                    />
                </Tab>
                <Tab title="Search">
                    <CardSearchContainer
                        addCard={this.props.addCard}
                        removeCard={this.props.removeCard}
                        />
                </Tab>
                <Tab title="Stats">
                    <p>stats coming soon...</p>
                </Tab>
            </TabContainer>
        );
    }
}

DeckMobileDisplay.propTypes = {
    deck: PropTypes.object.isRequired,
    addCard: PropTypes.func.isRequired,
    removeCard: PropTypes.func.isRequired
}

export default DeckMobileDisplay;

相关操作

export function createDeck(deck) {
    return dispatch => {
        dispatch(beginAjaxCall());

        const config = {
            method: 'POST',
            headers: { 'Content-Type' : 'application/json' },
            body : JSON.stringify({deck: deck})
        };

        return fetch(`http://localhost:3000/users/${deck.userId}/decks`, config)
            .then(res => res.json().then(deck => ({deck, res})))
            .then(({res, deck}) => {
                if (res.status >= 200 && res.status < 300) {
                    dispatch(createDeckSuccess(deck.deck));
                }
                else
                    dispatch(createDeckFailure(deck));
            })
            .catch(err => {
                console.log(err);
                dispatch(ajaxCallError(err));
            });
    };
}

export function updateDeck(deck) {
    return dispatch => {
        dispatch(beginAjaxCall());

        const body = JSON.stringify({deck: deck});

        const config = {
            method: 'PUT',
            headers : { 'Content-Type' : 'application/json' },
            body: body
        };

        return fetch(`http://localhost:3000/decks/${deck.id}`, config)
            .then(res => res.json().then(deck => ({deck, res})))
            .then(({res, deck}) => {
                if (res.status >= 200 && res.status < 300) {
                    dispatch(updateDeckSuccess(deck.deck));
                }
                    dispatch(ajaxCallError(err));
            })
            .catch(err => {
                console.log(err);
                dispatch(ajaxCallError(err));
            });
    };
}

export function updateDeckSuccess(deck) {
    return {
        type: types.UPDATE_DECK_SUCCESS,
        deck
    };
}

还有我的甲板Reducer

import * as types from '../actions/actionTypes';
import initialState from './initialState';

export default function deckReducer(state = initialState.decks, action) {
    switch (action.type) {
        case types.LOAD_USERS_DECKS_SUCCESS:
            return action.decks;

        case types.CREATE_DECK_SUCCESS:
            return [
                ...state,
                Object.assign({}, action.deck)
            ]

        case types.UPDATE_DECK_SUCCESS:
            return [
                ...state.filter(deck => deck.id !== action.deck.id),
                Object.assign({}, action.deck)
            ]

        default:
            return state;
    }
}

如果您需要查看该应用程序的更多信息,请访问此处:

https://github.com/dgravelle/magic-redux

如有任何帮助,我们将不胜感激,谢谢!

您的问题是因为您手动修改了组件的状态。 一个Redux的原则是:

State is read-only

The only way to change the state is to emit an action, an object describing what happened.

This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state. Because all changes are centralized and happen one by one in a strict order, there are no subtle race conditions to watch out for. As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes.

在方法removeCard中您正在修改状态:

removeCard(board, cardName) {
    let deck = this.state.deck;
    //This is just a reference, not a clone
    let cards = this.state.deck.cards;

    cards[board].forEach(i => {
         if(i.name === cardName) {
             if (i.quantity === 1) {
                 //Here you are modifying cards, which is a pointer to this.state.deck.cards
                 cards[board].splice(cards[board].indexOf(i), 1);
             }
             else {
                 //Here you are modifying cards, which is a pointer to this.state.deck.cards
                 i.quantity -= 1;
             }
         }
    });
    //... more stuff
}

您可能遗漏了一个概念,即 this.state.deck.cards 是数组内存位置的 reference/pointer。如果你想改变它,你需要克隆它。

一个解决方案可能是克隆原始数组:

removeCard(board, cardName) {
    let deck = this.state.deck;
    //Here you are cloning the original array, so cards references to a totally different memory position
    let cards = Object.assign({}, this.state.deck.cards);

    cards[board].forEach(i => {
         if(i.name === cardName) {
             if (i.quantity === 1) {
                 cards[board].splice(cards[board].indexOf(i), 1);
             }
             else {
                 i.quantity -= 1;
             }

         }
    });
    //... more stuff
}

希望对你有帮助。