0

I am very new to react and come across something which seems a little odd to me when setting state with hooks.

Given the state is configured like this:

const [games, setGames] = useState<Array<Game>>([]);

And I update it like this:

    const addGame = (gameId: string) => {
        setGames([...games, new Game(gameId)])
    }

After calling addGame for the second time, I would expect console.log(games.length) to return two values.

This is not the case and only the latest value is returned. I did some digging and noticed that people were using a lambda to achieve this instead and it does work.

    const addGame = (gameId: string) => {
        setGames(games => [...games, new Game(gameId)])
    }

But why? Can someone explain this, thanks.

Edit by HMR to address the comment

Here are adjustments I made to your code (see comments)

import React, { useState, useEffect, useCallback } from 'react';
import SetName from './SetName';
import { useCookies } from 'react-cookie';
import * as signalR from '@microsoft/signalr';
import { Game } from '../game/domain';
import { Card, Button, Row, Col } from 'antd';

export default function Lobby() {
  const [cookie] = useCookies(['swarm']);
  const [name, setName] = useState('');
  const [games, setGames] = useState([]);
  const [hubConnection, setHubConnection] = useState();
  //add swarmName variable
  const swarmName = cookie.swarm?.name;
  useEffect(() => {
    if (swarmName) setName(swarmName);
  }, [swarmName]); //depends on swarmName
  //moved addGame here so it can be a dependency of the effect
  //  if you don't put this in a useCallback the linter will warn you
  const addGame = useCallback((gameId) => {
    setGames((games) => [...games, new Game(gameId)]);
  }, []);
  useEffect(() => {
    //defined handler here so you can remove the listener
    //  in the effect cleanup function
    const handler = (gameId) => addGame(gameId);
    const createHubConnection = async () => {
      const hubConnect = new signalR.HubConnectionBuilder()
        .withUrl('/gamehub')
        .build();
      try {
        await hubConnect.start();
        console.log('Connection successful!');
      } catch (err) {
        alert(err);
        //rethrow the error for the effect cleanup
        return Promise.reject(err);
      }
      setHubConnection(hubConnect);

      await hubConnect.invoke('JoinLobby');

      hubConnect.on('addGame', handler);
      //returning hubConnect
      return hubConnect;
    };
    //using the return value (is a promise)
    const hubConnect = createHubConnection();
    //you forgot to return a function here that removes the listener
    return () =>
      //https://learn.microsoft.com/en-us/javascript/api/@aspnet/signalr/hubconnection?view=signalr-js-latest#off
      hubConnect.then((hubConnect) => hubConnect.off('addGame', handler));
  }, [addGame]); //when addGame is a useCallback the linter detects missing dependency

  let gameRows = games.map((game, index) => (
    <div key={index}>
      {game.id}
      <hr />
    </div>
  ));

  return (
    <Row>
      <Col className="gutter-row" flex="auto"></Col>
      <Col className="gutter-row">
        <SetName name={name} setName={setName}></SetName>
        <Card title="Games" style={{ marginTop: '20px', width: '300px' }}>
          <Button
            hidden={!cookie.swarm.name}
            style={{ marginTop: '7px' }}
            type="primary"
            onClick={() => hubConnection.invoke('CreateGame')}
            block
          >
            Create Game
          </Button>
          {gameRows}
        </Card>
      </Col>
      <Col className="gutter-row" flex="auto"></Col>
    </Row>
  );
}
HMR
  • 37,593
  • 24
  • 91
  • 160
4imble
  • 13,979
  • 15
  • 70
  • 125
  • 1
    Does this answer your question? [React Hook: setState usage](https://stackoverflow.com/questions/63058656/react-hook-setstate-usage) Basically it's used for capturing the previous state and modifying your array based on that, see more detailed answer in the mentioned SO link. – norbitrial Jul 30 '20 at 07:22
  • 1
    See also: [When to use React setState callback](https://stackoverflow.com/q/42038590) – Nick Parsons Jul 30 '20 at 07:26
  • I guess that's fine, it clears up how to use the previous state in the new state but im still confused as to why the second way of doing it is required and why the first one doesn't work. I guess i'll have to dig into the source code to find out :) – 4imble Jul 30 '20 at 07:31
  • Use functional state updates when next state depends on previous state, like appending values into an array. https://reactjs.org/docs/hooks-reference.html#functional-updates – Drew Reese Jul 30 '20 at 07:36
  • I think because useState is asynchronous, it's using the same value when using it twice at the same time, just like setState. see https://stackoverflow.com/questions/56433875/react-usestate-doesnt-update-value-synchronously – lam Jul 30 '20 at 08:19
  • @4imble `im still confused as to why the second way of doing it is required and why the first one doesn't work` Do you have a [snippet](https://codesandbox.io/s/great-fermat-li2ow) that demonstrates this problem? My guess it's because `setGames` ends up being a [stale closure](https://dmitripavlutin.com/react-hooks-stale-closures/) – HMR Jul 30 '20 at 09:58
  • Weirdly it works both ways in the snippet, but does not work within my project. Could be to do with it being a stale closure as you say, here's the source. https://github.com/4imble/swarm/blob/7144b3a0760fb73dd5eee473ebd696fdbb2a9e9b/Code/Swarm.Web.Client/src/pages/lobby/Lobby.tsx and the snippet where it works : https://codesandbox.io/s/cocky-snow-nlnij?file=/src/App.js – 4imble Jul 30 '20 at 11:19
  • @4imble You should use the `@` to direct comments to a specific person or nobody will get notified that you commented. I updated your question (cannot answer a closed one and can't open it again) with some suggestions to your code. – HMR Jul 30 '20 at 16:37
  • @4imble Your sansbox is just a standard react sandbox, no effect anywhere in the code. [Here](https://codesandbox.io/s/httpsstackoverflowcomq631680751641941-fxg7u?file=/src/index.js:1648-1686) is a sandbox that shows 2 working examples and one broken, the broken example has a linter warning. – HMR Jul 30 '20 at 17:17

0 Answers0