nionata / grapevine

Anonymous proximity-based gossip messaging mobile application
1 stars 0 forks source link

Root App Component State Rendering Issue #22

Open nionata opened 2 years ago

nionata commented 2 years ago

Our root component was originally a functional component with hooks. I noticed this odd behavior though. Every time setMessages was called the new message would be retained, but the previous array would be cleared. Additionally, the bluetooth manager would be reinitialized. Since it is inside of useEffect that could only mean that the component is being mounted or updated each time the state is changing. Super odd behavior and not desirable at all.

const App = () => {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    const bluetoothManager = new BluetoothManager(
      BluetoothMode.Advertise, 
      () => messages,
      (message: Message) => {
        console.log(messages, message)
        setMessages([...messages, message])
      }
    )
    bluetoothManager.start()

    return () => {
      // Cleanup logic
    }
  })

  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName: string = '';

            if (route.name === 'GrapeVine') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Peers') {
              iconName = focused ? 'bluetooth' : 'bluetooth-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'cog' : 'cog-outline';
            }

            // You can return any component that you like here!
            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: 'blueviolet',
          tabBarInactiveTintColor: 'gray',
        })}
      >
        <Tab.Screen name="GrapeVine" children={() => <HomeScreen messages={messages}/>} />
        <Tab.Screen name="Peers" component={ScanScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

export default App;

I tried to read up on functional components, but I wasn't enlightened much. I rewrote the component in the OG class syntax and it now it is working as desired. I'm going to leave the class version for now, but I would love to know what's going on here.

interface AppProps {
}
interface AppState {
  messages: Message[]
}

class App extends React.Component<AppProps, AppState> {
  constructor(props: AppProps) {
    super(props)
    this.state = {
      messages: []
    }
  }

  componentDidMount() {
    const bluetoothManager = new BluetoothManager(
      BluetoothMode.Advertise, 
      () => this.state.messages,
      (message: Message) => {
        console.log(this.state.messages, message)
        this.setState((state) => {
          return {
            messages: [...state.messages, message]
          }
        })
      }
    )
    bluetoothManager.start()
  }

  render() {
    return (
      <NavigationContainer>
        <Tab.Navigator
          screenOptions={({ route }) => ({
            tabBarIcon: ({ focused, color, size }) => {
              let iconName: string = '';

              if (route.name === 'GrapeVine') {
                iconName = focused ? 'home' : 'home-outline';
              } else if (route.name === 'Peers') {
                iconName = focused ? 'bluetooth' : 'bluetooth-outline';
              } else if (route.name === 'Settings') {
                iconName = focused ? 'cog' : 'cog-outline';
              }

              // You can return any component that you like here!
              return <Ionicons name={iconName} size={size} color={color} />;
            },
            tabBarActiveTintColor: 'blueviolet',
            tabBarInactiveTintColor: 'gray',
          })}
        >
          <Tab.Screen name="GrapeVine" children={() => <HomeScreen messages={this.state.messages}/>} />
          <Tab.Screen name="Peers" component={ScanScreen} />
          <Tab.Screen name="Settings" component={SettingsScreen} />
        </Tab.Navigator>
      </NavigationContainer>
    );
  }
}

export default App;
nionata commented 2 years ago

@raymondhechen any thoughts?

raymondhechen commented 2 years ago

In your useEffect function, are you sure messages is defined properly within the nested arrow function? You can test this by just console logging messages. If your new message is being retained but the old messages are not, then it's likely messages is undefined or empty in the setMessages call. This happens because the lines are executed within a separate scope.

useEffect(() => {
    const bluetoothManager = new BluetoothManager(
      BluetoothMode.Advertise, 
      () => messages,
      (message: Message) => {
        console.log(messages, message)
        setMessages([...messages, message])
      }
    )
    bluetoothManager.start()

    return () => {
      // Cleanup logic
    }
  })
raymondhechen commented 2 years ago

In your useEffect function, are you sure messages is defined properly within the nested arrow function? You can test this by just console logging messages. If your new message is being retained but the old messages are not, then it's likely messages is undefined or empty in the setMessages call. This happens because the lines are executed within a separate scope.

useEffect(() => {
    const bluetoothManager = new BluetoothManager(
      BluetoothMode.Advertise, 
      () => messages,
      (message: Message) => {
        console.log(messages, message)
        setMessages([...messages, message])
      }
    )
    bluetoothManager.start()

    return () => {
      // Cleanup logic
    }
  })

Potentially related: https://stackoverflow.com/questions/56511176/state-being-reset