5

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)

Thimma
  • 1,343
  • 7
  • 33
  • do you have any other effect that would need to run when `data` changes? – iamaatoh Jun 30 '20 at 14:09
  • yes, I got one, this is just demo code – Thimma Jun 30 '20 at 14:09
  • Hello. With your update (websocket) i found a similar example. And it's using Reducer ^^: https://adamrackis.dev/state-and-use-reducer/ . Hope it'll be usefull – rphlmr Jun 30 '20 at 14:10
  • i was going to suggest `const data = useRef({foo: [], bar: [] })` with the socket updating the `data.current` property values directly... but if you want other effects to run when the reference for `data` changes, then it's not feasible and wont work... – iamaatoh Jun 30 '20 at 14:12

3 Answers3

1

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:

Yousaf
  • 27,861
  • 6
  • 44
  • 69
  • I do not understand reducer at all, so I have no clue how to manage this.. – Thimma Jun 30 '20 at 13:58
  • 1
    i have added the implementation of reducer function which should give you an idea of how to implement `useReducer` hook in your code – Yousaf Jun 30 '20 at 14:19
1

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])

iamaatoh
  • 758
  • 5
  • 12
0

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 :) )

rphlmr
  • 838
  • 6
  • 12
  • I update my answer to be more precise. You'll have a memory leak only on unmount / remount ;). It can be really weird haha – rphlmr Jun 30 '20 at 14:24