Use useState value in useEffect without making the state update useEffect

Viewed 1987

I'm working on a function that manages a string array based on object keys. Let's say it looks like this:

import React, { useState, useEffect } from "react";
import FieldContext from "../contexts/FieldContext";
import io from "socket.io-client";

const [socket, setSocket] = useState(null);
// the `data` array gets changed every second due to a WebSocket, in case that's important
const [data, setData] = useState({ foo: [], bar: [] });
const [connections, setConnections] = useState(["conn1", "conn2"]);

const { checkedFields } = useContext(FieldContext); // ["foo", "moo"];

useEffect(() => {
  setConnections(prevConnections => {
    // The code below does the following: 
    // Loop through the temporary connections (which is a copy of checkedFields)
    // A. if `tempConn` is a key in the `data` object, push the name with `update_` prefix to the `_conns` array
    // B. If not, just push it without a prefix to the `_conns` array
    // Since the `checkedFields` array is ["foo", "moo"], the first element will get the prefix,
    // the other won't and will just get pushed.
    let _tempConns = [...checkedFields];
    let _conns = [];
    _tempConns.forEach(tempConn => {
      if (data[tempConn] !== undefined) _conns.push(`update_${tempConn}`);
      else _conns.push(tempConn);
    });
    return _conns;
  });
}, [checkedFields]);

// the websocket hook
useEffect(() => {
  const _socket = io(WS_URI);
  _socket.on("info", data => {
    // some magic happens here to add to the `data` object which is not important for this question
  });
  setSocket(_socket);
}, [])

I get the following warning while using this hook: React Hook useEffect has a missing dependency: 'data'. Either include it or remove the dependency array. Which I understand, but if I include data in the dependency array, I get an enormous amount of unnecessary updates. How do I prevent that from happening? (without using // eslint-disable-next-line please)

3 Answers

You are not only missing data from the dependency array of useEffect hook but you are also missing setConnections() function from the dependency array.

You can use useReducer hook to move the state update logic out of the useEffect hook.

const initialState = {
   data: { foo: [], bar: [] },
   connections: ["conn1", "conn2"]
} 

const reducerFunc = (state, action) => {
   switch(action.type) {
       case 'UPDATE_CONNECTIONS':
          let _tempConns = [...action.checkedFields];
          let _conns = [];
          _tempConns.forEach(tempConn => {
              if (state.data[tempConn] !== undefined) _conns.push(`update_${tempConn}`);
              else _conns.push(tempConn);
          });
          return { ...state, connections: _conns };
        default: 
          return state;
   }
};

const [myState, dispatch] = useReducer(reducerFunc, initialState);

const { checkedFields } = useContext(FieldContext); // ["foo", "moo"];

useEffect(() => {
    dispatch({ type: 'UPDATE_CONNECTIONS', payload: checkedFields });
}, [checkedFields]);

Since React makes sure that dispatch function won't change, you can omit it from useEffect hook's dependency array.

For details on how to use useReducer hook, see folowing links:

Could try with useRef if you can get away with the following in your websocket and there are no other useEffects counting on the data reference

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

// make `data` a ref object
// Pitfall: any changes to `data.current` will NOT be tracked 
const data = useRef({ foo: [], bar: [] })


// the websocket hook
useEffect(() => {
  ...
  _socket.on("info", data => {
    ...
    // update properties on `data.current`
    data.current.foo = 'some new value'
    data.current.bar = 'anoher new value;
  });
  
  ...
}, [])

useEffect(() => {
  setConnections(prevConnections => {
    ... 
    // reference data value from data.current
    const data = data.current

    ...
    return _conns;
  });
}, [checkedFields])

With your update, it seems that your socket never close. If your component unmount / remount, you add a new socket on background. And then on every updates, it fire all previous sockets.

Your socket hook should return a close socket function. (I guess, I never use socket directly)

  useEffect(() => {
  const _socket = io(WS_URI);
  _socket.on("info", data => {
    // some magic happens here to add to the `data` object which is not important for this question
  });
  setSocket(_socket);
  return () => {_socket.close();}
}, [])

(That's not the response of your answer but it can help :) )

Related