6

I didn't found any examples about how to fetch data from express server using react with socket.io.

Now i do something like this: Server.js

io.on('connection', socket => {
  console.log(socket.id)

  socket.on('disconnect', () => {
    console.log(socket.id + ' disconnected')
  })

  socket.on('load settings', () => {
    socket.emit('settings is here', data)
  })
})

React.js

const [socket] = useState(io())
  const [settings, setSettings] = useState(false)

   useEffect(() => {
    try {
      socket.emit('load settings');

      socket.on('settings is here', (data) => {
        // we get settings data and can do something with it
        setSettings(data)
      })
    } catch (error) {
      console.log(error)
    }
  }, [])
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
ZiiMakc
  • 31,187
  • 24
  • 65
  • 105
  • 1
    This looks like it would work. Is it failing? Also you don't need to pass that empty array to `useEffect`. It diffs variables passed into that array to decide whether or not to execute the effect callback. – James Nov 19 '18 at 22:09
  • 1
    yes, it's working, i just don't know is it a right way! First time trying sockets. If i will not pass empty array, it's will rerender every sec. With empty array it's like to say "do only onse". – ZiiMakc Nov 19 '18 at 22:48
  • Oh thats a good point about only wanting this to execute once. Anyways, this looks fine to me. – James Nov 19 '18 at 22:55

2 Answers2

14

This looks fine, but there are some things you can improve on, such as disconnecting the socket before unmounting and not making the socket part of state (refer to the code example below).

If you're confused over how to port existing code to hooks, write out the component using classes first, then port part by part to hooks. You could refer to this StackOverflow answer as a cheatsheet.

Using traditional classes, using socket.io looks like:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.socket = io();
  }

  componentDidMount() {
    this.socket.open();
    this.socket.emit('load settings');
    this.socket.on('settings is here', (data) => {
      // we get settings data and can do something with it
      this.setState({
        settings: data,
      })
    });
  }

  componentWillUnmount() {
    this.socket.close();
  }

  render() {
    ...
  }
}

Then you can port the this.socket to use useRef (it doesn't need to be part of state as your render() function doesn't need it. So useRef is a better alternative (although useState is likely to still work).

Port componentDidMount() via using useEffect and passing an empty array as the second argument to make the effect callback only run on mount.

Port componentWillUnmount() via returning a callback function in the useEffect callback which React will call before unmounting.

function App() {
  const socketRef = useRef(null);
  const [settings, setSettings] = useState(false);

  useEffect(() => {
    if (socketRef.current == null) {
      socketRef.current = io();
    }

    const {current: socket} = socketRef;

    try {
      socket.open();
      socket.emit('load settings');
      socket.on('settings is here', (data) => {
        // we get settings data and can do something with it
        setSettings(data);
      })
    } catch (error) {
      console.log(error);
    }
    // Return a callback to be run before unmount-ing.
    return () => {
      socket.close();
    };
  }, []); // Pass in an empty array to only run on mount.

  return ...;
}
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
  • Thank you very match, i understand classes. One more question, i need to share same socket to other components that will want to fetch their own data. So, is it a good idea to just share this socket ref in provider? – ZiiMakc Nov 20 '18 at 08:19
  • 1
    You could create a top level component that communicates with the sockets and passes down the data via context. I don't think the other components should be aware and reference the socket. – Yangshun Tay Nov 20 '18 at 08:24
  • 1
    This is the best example of a well-documented functional implementation with web sockets that I've seen. Thank you very much! – carl Jan 21 '20 at 01:34
  • @YangshunTay What is the reason of using `useRef` to setup the websocket rather than declaring it as a variable (ex. `let socket = io()`)? – James Apr 01 '21 at 12:43
  • 1
    @James You would want to reference the same socket object throughout the component's lifetime. If you declare it as a variable within the function, a new instance gets reinitialized on every render and your socket will disconnect. Declaring outside the function is also bad as you can only have one instance for the entire page (all instances of the components will share that same socket instance. – Yangshun Tay Apr 01 '21 at 16:05
  • 1
    Very good example. However, warning with that: ```const { current: socket } = useRef(io());``` Multiple connections are created on each render (even if the socket reference remains the same). We should instantiate the socket inside the effect to avoid this bad behavior. – Thibault Boursier Jul 14 '21 at 17:44
  • Hi, will it be possible to then use that socket ref in a 2nd useEffect? – kd12345 Oct 16 '22 at 12:54
7

The accepted answer has the downside, that the initial state of the useRef() gets called on every re-render. With a text input for example, a new connection is established on every input change. I came up with two solutions:

  1. Define the socket in the useEffect
const ChatInput = () => {
  const [chatMessage, setChatMessage] = useState<string>('');
  const socket = useRef<Socket>();

  useEffect(() => {
    socket.current = io('my api');
    socket.current.on('chat message', (message: string) => {
      setChatMessage(message);
    });
    return () => { socket.current?.disconnect(); };
  }, []);

  const inputHandler = (text: string) => {
    socket.current?.emit('chat message', text);
  };

  return (
    <View>
      <Text>{chatMessage}</Text>
      <TextInput onChangeText={(text) => inputHandler(text)} />
    </View>
  );
};
  1. Define the socket.io in a useState()
const ChatInput = () => {
  const [chatMessage, setChatMessage] = useState<string>('');
  const [socket] = useState(() => io('my api'));

  useEffect(() => {
    socket.on('chat message', (message: string) => {
      setChatMessage(message);
    });
    return () => { socket.disconnect(); };
  }, []);

  const inputHandler = (text: string) => {
    socket.emit('chat message', text);
  };

  return (
    <View>
      <Text>{chatMessage}</Text>
      <TextInput onChangeText={(text) => inputHandler(text)}/>
    </View>
  );
};

export default ChatInput;
Fabian
  • 188
  • 2
  • 5
  • 1
    I voted for this response because it solves the multiple connection problem. And, I have a preference for the useRef one. useState have to be used with states that changes rendering not Refs. – jarjar Feb 27 '21 at 01:38
  • @Fabian What is the reason of using `useRef` to setup the websocket rather than declaring it as a variable (ex. `let socket = io()`)? – James Apr 01 '21 at 12:45
  • @James Read this article from the Christ of react and understand the philosophy of react.js https://overreacted.io/react-as-a-ui-runtime/ – snehanshu.js May 14 '21 at 21:14
  • Hi @James. Given we define the `let socket` in a useEffect, as soon as the component mounts (eg. useEffect(() => {socket = io()}, []}), then we still avoid the connection problem. In this case though, the `socket` variable will be `undefined` on any rerender (since it is only defined on mount). Now we store our messages in a state, which causes a rerender on state change. This is why we need to "persist" the connection. With useRef we can persist the socket throughout the lifetime of the component. – Fabian May 16 '21 at 17:53
  • Thanks Fabian, the second solution solves my constant connections on re-render. And it works perfectly using redux with RTK as: socket.on('tickers', (tickers) => { dispatch(setTickers(tickers)) }) – Banzy Dec 01 '21 at 13:08