1

Sorry if this is a bit of a noob question. I'm trying to set up a react-native multiplayer game with a lobby which shows who your opponents are. The problem is that even when the prop representing the opponents updates, the component does not re-render, even though a console log shows me the prop has changed (and that render() was called?). Some snippets from my code are below:

In Lobby.js:

export default class Lobby extends Component {
  render() {
    console.log('In Lobby, opponents = ' + this.props.opponents);
    return(
      <View style={{ flex: 1, justifyContent: "center", alignItems: 'center' }}>
        <Text>Welcome to the lobby { this.props.username }!</Text>
        <Text>Opponents: { this.props.opponents }</Text>
        ... more text and buttons ...
      </View>
    );
  }
}

As I said, I can see in the console that this.props.opponents does change but the screen doesn't seem to re-render that <Text> component. I initially thought this may be because this.props.opponents is set to the opponents value of my Main component's state, but the console log seems to debunk this.

I've looked here on SO and found suggestions to add shouldComponentUpdate() and componentDidUpdate() to my component but the nextProps parameter would always actually be the same as this.props. I.e. if I add:

shouldComponentUpdate(nextProps) {
  console.log('this.props.opponents=' + this.props.opponents + '; ' + 'nextProps.opponents=' + nextProps.opponents);  
  return this.props.opponents != nextProps.opponents;
}

This never actually returns True for me, even though the prop is definitely changing. (I also don't know what code I would actually want in componentDidUpdate() either, since I just want it to re-render). It just shows that, yes the prop is different but that both nextProps.opponents and this.props.opponents have changed to that same new value (i.e. 'TestUser').

I'm really at a loss here. Thanks in advance.

EDIT: Added simplified version of my code.

App.js:

import React, { Component } from 'react';

import Main from './components/Main';

export default function App() {
  return (
    <Main/>
  )
}

In Main.js:

import React, { Component } from 'react';
import { View } from 'react-native';

import Lobby from './Lobby';

export default class Main extends Component {
  constructor(props) { 
    super(props);

    this.state = { 
      opponents: [], // array of strings of other users' usernames
      in_lobby: true // whether user is in lobby or not
    };

    this.testClientJoin = this.testClientJoin.bind(this);
  }

  testClientJoin() {
    console.log('Called testClientJoin().');
    let currentOpponents = this.state.opponents;
    currentOpponents.push('TestUser');
    this.setState({
      opponents: currentOpponents
    });
  }

  render() {
    return(
      <View style={{ flex: 1}}>
        {
          this.state.in_lobby &&
          <Lobby
            opponents={this.state.opponents}
            testClientJoin={this.testClientJoin}
          />
        }
      </View>
    );
  }
}

In Lobby.js:

import React, { Component } from 'react';
import { View, Button, Text } from 'react-native';

export default class Lobby extends Component {
  render() {
    console.log('Opponents prop = ' + this.props.opponents);
    return(
      <View style={{ flex: 1, justifyContent: "center", alignItems: 'center' }}>
        <Text>Welcome to the lobby!</Text>
        <Text>Your opponents are {this.props.opponents}.</Text>
        <Button
          title='Add test opponent'
          onPress={this.props.testClientJoin}
        />
      </View>
    );
  }
}

So. When I tap the button to Add test opponent, the console logs

Called testClientJoin().
Opponents prop = TestUser

which shows the prop has indeed updated. However, the doesn't actually reflect this change, until I force some update (i.e. since I'm using expo, I just save the file again, and I finally see the text appear on my phone. I'm sure I'm just being an idiot but would love to know how to get the desired behavior of the text component updating.

Lex
  • 51
  • 5
  • Your title describes the correct behavior -- when the props change the component should re-render. It would be helpful to have a self-contained example that you can fully post that demonstrates the issue; it seems something you're not posting must be affecting it. In particular I'm concerned about what the type of opponents is. – jack Feb 18 '20 at 23:28
  • 1
    @jack. I'll mock up a test-app and update the post when it's done. opponents is a simple array and does actually render if I leave the lobby screen, then go back, but yeah, I'll try make a full example. – Lex Feb 18 '20 at 23:43
  • Ah, if it's an array then that explains why you're never seeing true for the difference across prop updates -- that's not a good way to check array equality. See ie: https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript – jack Feb 18 '20 at 23:47
  • The thing is, they are actually equivalent. Somehow, this.props.opponents already equals ['TestUser'] when nextProps.opponents equals ['TestUser']. The issue must be passing in the Main component's state as a prop to the Lobby component. – Lex Feb 18 '20 at 23:59
  • Sure, from our perspective they seem equivalent, but you understand why it's always returning false, right? – jack Feb 19 '20 at 00:01
  • Not exactly, sorry. I'm not sure if you're hinting at them not being the same object, but I used the arraysEqual function from the link you provided which shows them to be equal. And logging from the shouldComponentUpdate, shows this.prop.opponents is already equal to nextProps.opponents, which I still think is the issue, but I'm working on the test app now. – Lex Feb 19 '20 at 00:06
  • @jack, you can see the edit to see a self-contained app showing what I mean. – Lex Feb 19 '20 at 00:25

1 Answers1

1

Well I found an answer to my question which I'll leave here if anyone finds this. What I had to do was the following:

Main.js: Instead of passing this.state.opponents directly as a prop to <Lobby>, pass a copy of this array instead.

render() {
  <Lobby
    testClientJoin={this.testClientJoin}
    opponents={ [...this.state.opponents] }
  />

And this has the desired result. So the moral of the story is, don't try to directly pass an array state from a parent to a child component.

Lex
  • 51
  • 5