简体   繁体   中英

React + Redux: component not rerendering on state change

I'm making a multiplayer game on React and using Colyseus.js as my multiplayer API. The thing is that I need to display all connected players in the lobby:

export default function Lobby() {
    const classes = useStyles();
    const gameState = useSelector(state => state.roomState);
    const history = useHistory();

    // Creating array to display all connected players
    let playersArray = [];
    if(Object.entries(gameState).length !== 0) {
        playersArray = Object.values(gameState.players.toJSON());

    }
    // Check if game has started
    useEffect(()=>{
        console.log('gamestarted:', gameState.gameStarted);
        if(gameState.gameStarted){
            history.push('/game')
        }
    }, [gameState.gameStarted]);
    return (
        <PageFrame>
            <Grid
                container
                direction="column"
                alignItems="center"
                className={classes.root}
                spacing={2}
            >
                <Grid item xs={12}>
                    <Typography className={classes.gameTitle}>
                        {(gameState != null) ? gameState.gameName : <CircularProgress />}
                    </Typography>
                </Grid>
                <Grid item xs={12}>
                    Waiting for all players to join...
                </Grid>
                <Grid item xs={12}>
                    Connected Players: {playersArray.length}
                </Grid>
                <Grid item xs={12}>
                    {playersArray.map((data, index) => {
                        return (
                            <Chip
                                key={index}
                                label={data.party}
                                className={classes.chip}
                                style={{"--color": green[500]}}
                            />
                        );
                    })}
                </Grid>
                <Grid item xs={12}>
                </Grid>
            </Grid>
        </PageFrame>
    );
}

I use useSelector to get state I received from Colyseus.js and dispatched like this (in another component which has Lobby as a child in <Route> ):

const joinRoom = (id, partyName, actions) => {
        client.joinById(id, {partyName, id}).then(room => {
            dispatch({type: GAME_ROOM, payload: room });
            dispatch({type: RECONNECT_PARAMS, payload: {roomId: room.id, sessionId: room.sessionId} });
            // Redirect to lobby
            history.push('/lobby')
            {...}
            room.onStateChange((state) => {
                console.log("the room state has been updated:", state);
                // HERE DISPATCHING RECEIVED STATE
                dispatch({type: GAME_STATE, payload: state });
            });
            room.onLeave((code) => {
                {...}
        });
    }

state from server has property players with object, which has keys with player names I need to display. My reducer:

const initialState = {
    joinDetails:{
        gameCode: '',
        partyName: ''
    },
    sessionParams: {
        roomId: "",
        sessionId: "",
    },
    room: {},
    roomState: {},
    scoreChange: null,
    totalChange: null,
    playerState:{
        showResults: false,
        questions: [],
        totalBudget: 5000,
        totalGlobalBudget: 5000,
        budget: [],
        activeStep: 0,
        stepCompleted: [false,false,false,false,false],
        decisions: [-1,-1,-1,-1],

    }

};

function rootReducer(state = initialState, action) {

    switch (action.type) {
        case JOIN_FORM:
            return {...state, joinDetails: action.payload};
        case GAME_ROOM:
            return {...state, room: action.payload};
        case GAME_STATE:
            return {...state, roomState: action.payload};
        case RECONNECT_PARAMS:
            return {...state, sessionParams: action.payload};
        case CHANGE_STEP:
            if(action.payload === 'add'){
                return {...state, playerState: {...state.playerState, activeStep: state.playerState.activeStep + 1}}}
            else{
                return {...state, playerState: {...state.playerState, activeStep: state.playerState.activeStep - 1}}}
        case COMPLETE_STEP:
            return {...state, playerState: {...state.playerState, stepCompleted: action.payload}};
        case SET_BUDGET:
            return {...state, playerState: {...state.playerState, budget: action.payload}};
        case SET_QUESTIONS:
            return {...state, playerState: {...state.playerState, questions: action.payload}};
        case SET_TOTAL_BUDGET:
            if(action.payload.add !== undefined){
                return {...state, playerState: {...state.playerState, totalBudget: state.playerState.totalBudget + action.payload.add}}
            }
            else{
                return {...state, playerState: {...state.playerState, totalBudget: action.payload}};
            }

        case SET_GLOBAL_BUDGET:
            if(action.payload.add !== undefined){
                return {...state, playerState: {...state.playerState, totalGlobalBudget: state.playerState.totalGlobalBudget + action.payload.add}}
            }
            else{
                return {...state, playerState: {...state.playerState, totalGlobalBudget: action.payload}}
            }
        case SET_DECISIONS:
            return {...state, playerState: {...state.playerState, decisions: action.payload}};
        case SET_CHANGES:
            return {...state, scoreChange: action.payload.change, totalChange: action.payload.total};
        case SHOW_RESULTS:
            return {...state, playerState: {...state.playerState, showResults: action.payload}};
        case CLEAR_STATE:
            return {...state, playerState: {}};

        default:
            return state;
    }
}
export default rootReducer;

The problem is that player list does not update after new players join. I can confirm that:

  • I receive state from server with new players
  • I dispatch this state to redux successfully (I see it through redux devtools)
  • It looks like Lobby() is simply not rerendering, dispite having useSelector which works, because each player sees the list of players who joined before him.

I tried looking for mutated state, but it looks to me I everything is good there. It's my first redux project, so maybe I'm missing something. One suspicious thing I notice in redux devtools that sometimes(,) the latest state propagates trough all previous states in devtools view. eg, "Diff" tab shows that all same actions resulted in no changes except the first one has changed state to final form: for example:

Actual changes:

  • ACTION changed some property to a
  • ACTION changed some property to b
  • ACTION changed some property to c

Shown changes (but they became like this only after third time):

  • ACTION changed some property to a, some property to b and some property to c
  • ACTION changed nothing
  • ACTION changed nothing

Not sure if this normal or causes my issue.

If you need aditional code let me know.

Thanks.

EDIT: Changed some names, as per advice in comments, issue still perisits.

EDIT 2: The problem seems directly related to useSelector : currently I have

gameState = useSelector(state => state.roomState)

but if I select the whole state, everything works, component rerenders.

gameState = useSelector(state => state)

this is far from ideal: now I need to call gameState.roomState.something instead of gameState.something everywhere. How it can be that useSelector doesn't recognize the change of particular part of state and only recognize the change of whole state?

So my problem was here:

room.onStateChange((state) => {
                console.log("the room state has been updated:", state);
                // HERE DISPATCHING RECEIVED STATE
                dispatch({type: GAME_STATE, payload: state });

I was putting state to payload without destructuring/copying it to new object, thus actually only passing a reference. And of course, when changed it also changed all other references and there was nothing to compare to. The correct way is destructuring received state while passing it to action:

room.onStateChange((state) => {
                console.log("the room state has been updated:", state);
                // HERE DISPATCHING RECEIVED STATE
                dispatch({type: GAME_STATE, payload: {...state} })

After your second edit, I had a better look at your action to find why state.roomState hasn't change for trigger the useSelector, and can't believe it, the exact same question I answered two weeks ago - How do I dispatch multiple actions in react-native using hooks?

you can attach more than one payload to dispatch. Change joinRoom action to one dispatch hold both "GAME_ROOM" and "RECONNECT_PARAMS"


 client.joinById(id, {partyName, id}).then(room => {
        dispatch({
              type: 'GAME_ROOM',
              room,
              params: {roomId: room.id, sessionId: room.sessionId} 
     });
  ..........

and then in the reducer combine them to one case listener


 case 'GAME_ROOM':
        return {...state, room: action.room, sessionParams: action.params }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM