0

I am trying to receive data from a websocket url by using the web3 library. However, React Hooks frustrates me, because it re-renders the whole App function when updating my array by using the useState set function. How can I separate my render function from the websocket subscription? I don't want to re-subscribe every single time when using setState.

Here is my code:

function App() {

console.log('init');
const [blocks, setBlock] = useState([]);

(async () => {
const web3 = new Web3(new

  Web3.providers.WebsocketProvider('wss...'));

web3.eth.subscribe('newBlockHeaders', async (error, blockHeader) => {

  const block = await web3.eth.getBlock(blockHeader.hash, true);

  setBlock([...blocks, block.number]);
  console.log(blocks);

});
})();


return ( 
<div className = "App">
  <header className = "App-header">
    <p>{ blocks }</p>
  </header> 
</div>
);
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
Logan
  • 93
  • 9

2 Answers2

2

Use a mounting useEffect hook (i.e. w/empty dependency array) to handle the subscription. Return a cleanup function to unsubscribe. The useEffect hook runs once to setup the subscription, and when the component unmounts calls the cleanup function to unsubscribe.

Use a functional state update to add new block numbers to the blocks state. The functional state update avoids the stale enclosure of the blocks state value when the subscription's newBlockHeaders event listener callback is set, it passes in the previous state to update from.

useEffect

useState functional updates

function App() {
  const [blocks, setBlock] = useState([]);

  useEffect(() => {
    const web3 = new Web3(new Web3.providers.WebsocketProvider('wss...'));

    const sub = web3.eth.subscribe(
      'newBlockHeaders',
      async (error, blockHeader) => {
        const block = await web3.eth.getBlock(blockHeader.hash, true);
        setBlock(blocks => [...blocks, block.number]);
      },
    );

    return () => sub.unsubscribe();
  }, []);

  return ( 
    <div className = "App">
      <header className = "App-header">
        <p>{ blocks }</p>
      </header> 
    </div>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks for your support! I am trying to avoid unsubscribing and also update the React views by not affecting the UseEffect content. Is there a way to achieve this? – Logan Dec 14 '21 at 06:11
  • @Logan I'm sorry, I don't quite follow that comment. You *should* unsubscribe in the cleanup function when unmounting otherwise it's a potential memory leak. It is standard practice with the `useEffect` hook for side-effects like adding event listeners and subscriptions, etc... they should be cleaned up optionally between render cycles, and at the end of the component's life when it's unmounted. – Drew Reese Dec 14 '21 at 06:14
  • What I am trying to explain is that I want to continuously receive data as a stream in order to update the React Renderer. So every time when receiving data, the renderer should update based on the blocks array state. However, I notice that the useEffect is being called over and over again when we return the sub. Why can't we just separate the renderer from the subscription without subscribing over and over again? – Logan Dec 14 '21 at 06:20
  • @Logan This is what the `useEffect` hook ***without*** dependencies is for. They run once when the component mounts, subscribes... and when the component unmounts, cleans up the subscription. The event callback handles updating the state and triggering rerenders. See [timing of effects](https://reactjs.org/docs/hooks-reference.html#timing-of-effects). – Drew Reese Dec 14 '21 at 06:22
  • Oh God.. So there is no way to separate the renderer from the continuous stream? I have to mount and unmount every single time when receiving data from the websocket listener? – Logan Dec 14 '21 at 06:25
  • @Logan No, not at all. Mount once, subscribe once, the continuous stream triggers all the rerenders it needs to when it updates state, then unmount once, unsubscribe once. This is all related to the React component lifecycle. – Drew Reese Dec 14 '21 at 06:27
  • Thanks a lot for your swift response. Appreciate your help. Is there a way that I can reach out to you to talk about this? – Logan Dec 14 '21 at 06:30
1

I think you should use useCallback and useEffect

const subscribe = useCallback(async () => {
    const web3 = new Web3(new Web3.providers.WebsocketProvider('wss...'));

    web3.eth.subscribe('newBlockHeaders', async (error, blockHeader) => {

        const block = await web3.eth.getBlock(blockHeader.hash, true);

        setBlock([...blocks, block.number]);
        console.log(blocks);

    });
}, [])

useEffect(() => {
    subscribe()
}, [])
Huan Le
  • 206
  • 2
  • 5