Data coming in from socket doesn't update the state with useEffect - reactjs

See demo here
I'm connecting to sockets (modelled by setTimeouts!) and getting an array. On mount I get an initial array. Then I keep listening for updates to the array. An update is sent as just the change and not a whole array.
I need to access the current state, but it's empty. Even though it looks fine in the render.
I think this might be a scoping or closure bug caused by numbers being empty at the time of calling addLater(), but I'm not sure what the solution is.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [numbers, setNumbers] = useState([]);
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"]);
// incoming messages from socket
addLater();
addMuchLater();
}, []);
const addLater = () => {
window.setTimeout(() => {
console.log("Why is the state empty? ", numbers);
const changedNumbers = [...numbers];
changedNumbers.splice(1, 1, "fourth");
setNumbers(changedNumbers);
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
const changedNumbers = [...numbers];
changedNumbers.splice(2, 1, "fifth");
setNumbers(changedNumbers);
}, 10000);
};
return (
<div className="App">
{numbers.map((r, i) => (
<p>
{i}: {r}
</p>
))}
</div>
);
}

When the next value depends on the previous one it's best to write the code as a functional update so the code will always be acting on the latest value. As you ran into, your current code closes over the original value of numbers, which isn't what you want:
const addLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(1, 1, "fourth");
return changedNumbers;
});
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(2, 1, "fifth");
return changedNumbers;
});
}, 10000);
};

setState is asynchronous so the calls to state that happen in the functions later on get the state as it was when the component rendered, not as it is after the new state was set. You can use a callback function as the second argument:
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"],
()=>{
// incoming messages from socket
addLater();
addMuchLater();
}), []);
See:
https://upmostly.com/tutorials/how-to-use-the-setstate-callback-in-react

Related

React useState is not updating when set method is called in an interval

I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);

First fetching data is successfull, second fetching not

I want to fetch data with useEffect. With the first function, I get event details and with the second function, I get questions, which are related to the event (by ID). Fetching data for the event is successful, but second fetching data for the questions is not successful (sometimes is successful). Why fetching data once is successful, but the second time is not successful?
The output of console log:
Quiz.js:23 {active: true, _id: "6012eafe7813901034e77fb3", nameOfEvent: "Udalost 3", creatorId: "5fd23fb7b1a3a005cc82225d", participants: Array(0), …}
Quiz.js:45 {}
Quiz.js:23 {active: true, _id: "6012eafe7813901034e77fb3", nameOfEvent: "Udalost 3", creatorId: "5fd23fb7b1a3a005cc82225d", participants: Array(0), …}active: truecodeEvent: "udalost1"createdAt: "2021-01-28T16:49:02.374Z"creatorId: "5fd23fb7b1a3a005cc82225d"nameOfEvent: "Udalost 3"participants: []updatedAt: "2021-02-01T16:52:45.471Z"__v: 0_id: "6012eafe7813901034e77fb3"__proto__: Object
Quiz.js:45 {}
File Quiz.js
const Quiz = () => {
const dataForQuiz = getUserDataToQuiz();
const [eventValues, setEventValues] = useState({});
const [questionBankOfEvent, setQuestionBankOfEvent] = useState({});
const initDataToEvent = () => {
getEventByEventCode(dataForQuiz.codeEvent).then(data => {
if (!data.error) {
setEventValues(data);
}
else {
console.log(data.error);
}
})
}
console.log(eventValues);
const initQuestionsForQuiz = () => {
if (eventValues) {
getQuestionsToEvent(eventValues._id).then((data) => {
if (!data.error) {
setQuestionBankOfEvent(data);
}
else {
console.log(data.error);
}
})
}
}
useEffect(() => {
initDataToEvent();
if(eventValues){
initQuestionsForQuiz();
}
}, []);
console.log(questionBankOfEvent);
const formQuiz = () => {
return(
<>
<h1>Quiz</h1>
</>
);
}
return (
<>
{formQuiz()}
</>
);
}
export default Quiz;
On the first render of your component, you execute
initDataToEvent();
if(eventValues){
initQuestionsForQuiz();
}
initDataToEvent is supposed to update eventValues. So you expect initQuestionsForQuiz to be executed.
But, by the time if(eventValues) is evaluated, setEventValues has been fired, but the eventValues state has not been modified yet.
Hence, if you modify your code to:
useEffect(() => {
initDataToEvent();
console.log(eventValues);
if(eventValues){
initQuestionsForQuiz();
}
}, []);
You would observe eventValues to be empty.
In React, it is best practice to split events and logic. In your case, you want initQuestionsForQuiz to be run when eventValues has been updated and not empty. Modifying your code to:
useEffect(() => {
initDataToEvent();
}, []);
useEffect(() => {
if(eventValues){
initQuestionsForQuiz();
}
}, [eventValues]);
should make it work. What this code does is that the function given as argument to the seconde useEffect will be run every time eventValues is modified.
Probably, due to the fact that both are in the same useEffect, since react doesn´t update useState instantly. I would separate them in two different useEffect, being:
useEffect(() => {
initDataToEvent();
}, []);
useEffect(() => {
if(eventValues){
initQuestionsForQuiz();
}
}, [eventValues]);

React State value not updated in Arrow functional component

React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);

Reconnecting web socket using React Hooks

I want to establish a websocket connection with the server. Reconnect after 5 seconds if the connection closes. I am using React Hooks and so far achieved this
import React, { useRef, useState, useEffect } from 'react';
function App() {
const wsClient = useRef(null);
const [wsState, setWsState] = useState(true)
useEffect(() => {
    wsClient.current = new WebSocket(url);
    console.log("Trying to open ws");
    setWsState(true)
    
    wsClient.current.onopen = () => {
      console.log('ws opened');
      wsClient.current.send('{"type" : "hello"}')
    };
    wsClient.current.onclose = (event) => {
// Parse event code and log
      setTimeout(() => {setWsState(false)}, 5000)
      console.log('ws closed');
}
wsClient.current.onmessage = ((event) => {
      // DO YOUR JOB
})
return () => {
      console.log('ws closed');
      wsClient.current.close();
    }
  }, [wsState]);
return (
      <div className="App">
        <Header />
        <MainBody />
      </div>
  );
}
This is creating exponentially increasing number of retries when it is unable to connect with server, if I remove setTimeout and use simple setState it is working normally.
I am unable to understand the issue and also suggest what is the best practice to achieve my goal.
I'm not convinced that an effect is the best place for this. If it's application-level, it may be simpler to implement it in its own module, and bring that in, where needed.
Nevertheless, to get this to work, you should consider that you're managing two separate lifecycles: the component lifecycle, and the websocket lifecycle. To make it work as you want, you have to ensure that each state change in one aligns with a state change in the other.
First, keep in mind that your effect runs every time the dependencies in the array change. So, in your example, your effect runs every time you set wsState.
The other thing to keep in mind is that your cleanup function is called every time wsState changes, which you're doing twice in your effect (setting it to true on open, and false on close). This means that when you create a new socket, and it fails to connect, the close event fires, and it queues up a state change.
Each time it attempts to connect, it sets wsState to true (which queues a re-run of your effect), tries and fails to connect, finally setting another timeout, which updates the state to false. But, not before the effect runs again, trying to set the state to true, etc.
To fix this, start with the effect lifecycle. When should your effect run? When should it be cleaned up? A few thoughts:
The effect should run once during the first render, but not during subsequent renders
The effect should be cleaned up when the WebSocket disconnects
The effect should be re-run after a timeout, triggering a reconnect
What does this mean for the component? You don't want to include the WS state as a dependency. But, you do need state to trigger it to re-run after the timeout.
Here's what this looks like:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
export default function App() {
const clientRef = useRef(null);
const [waitingToReconnect, setWaitingToReconnect] = useState(null);
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
function addMessage(message) {
setMessages([...messages, message]);
}
useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
if (!clientRef.current) {
const client = new WebSocket(URL);
clientRef.current = client;
window.client = client;
client.onerror = (e) => console.error(e);
client.onopen = () => {
setIsOpen(true);
console.log('ws opened');
client.send('ping');
};
client.onclose = () => {
if (clientRef.current) {
// Connection failed
console.log('ws closed by server');
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
console.log('ws closed by app component unmount');
return;
}
if (waitingToReconnect) {
return;
};
// Parse event code and log
setIsOpen(false);
console.log('ws closed');
// Setting this will trigger a re-run of the effect,
// cleaning up the current websocket, but not setting
// up a new one right away
setWaitingToReconnect(true);
// This will trigger another re-run, and because it is false,
// the socket will be set up again
setTimeout(() => setWaitingToReconnect(null), 5000);
};
client.onmessage = message => {
console.log('received message', message);
addMessage(`received '${message.data}'`);
};
return () => {
console.log('Cleanup');
// Dereference, so it will set up next time
clientRef.current = null;
client.close();
}
}
}, [waitingToReconnect]);
return (
<div>
<h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
{waitingToReconnect && <p>Reconnecting momentarily...</p>}
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
In this example, the connection state is tracked, but not in the useEffect dependencies. waitingForReconnect is, though. And it's set when the connection is closed, and unset a time later, to trigger a reconnection attempt.
The cleanup triggers a close, as well, so we need to differentiate in the onClose, which we do by seeing if the client has been dereferenced.
As you can see, this approach is rather complex, and it ties the WS lifecycle to the component lifecycle (which is technically ok, if you are doing it at the app level).
However, one major caveat is that it's really easy to run into issues with stale closures. For example, the addMessage has access to the local variable messages, but since addMessage is not passed in as a dependency, you can't call it twice per run of the effect, or it will overwrite the last message. (It's not overwriting, per se; it's actually just overwriting the state with the old, "stale" value of messages, concatenated with the new one. Call it ten times and you'll only see the last value.)
So, you could add addMessage to the dependencies, but then you'd be disconnecting and reconnecting the websocket every render. You could get rid of addMessages, and just move that logic into the effect, but then it would re-run every time you update the messages array (less frequently than on every render, but still too often).
So, coming full circle, I'd recommend setting up your client outside of the app lifecycle. You can use custom hooks to handle incoming messages, or just handle them directly in effects.
Here's an example of that:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
function reconnectingSocket(url) {
let client;
let isConnected = false;
let reconnectOnClose = true;
let messageListeners = [];
let stateChangeListeners = [];
function on(fn) {
messageListeners.push(fn);
}
function off(fn) {
messageListeners = messageListeners.filter(l => l !== fn);
}
function onStateChange(fn) {
stateChangeListeners.push(fn);
return () => {
stateChangeListeners = stateChangeListeners.filter(l => l !== fn);
};
}
function start() {
client = new WebSocket(URL);
client.onopen = () => {
isConnected = true;
stateChangeListeners.forEach(fn => fn(true));
}
const close = client.close;
// Close without reconnecting;
client.close = () => {
reconnectOnClose = false;
close.call(client);
}
client.onmessage = (event) => {
messageListeners.forEach(fn => fn(event.data));
}
client.onerror = (e) => console.error(e);
client.onclose = () => {
isConnected = false;
stateChangeListeners.forEach(fn => fn(false));
if (!reconnectOnClose) {
console.log('ws closed by app');
return;
}
console.log('ws closed by server');
setTimeout(start, 3000);
}
}
start();
return {
on,
off,
onStateChange,
close: () => client.close(),
getClient: () => client,
isConnected: () => isConnected,
};
}
const client = reconnectingSocket(URL);
function useMessages() {
const [messages, setMessages] = useState([]);
useEffect(() => {
function handleMessage(message) {
setMessages([...messages, message]);
}
client.on(handleMessage);
return () => client.off(handleMessage);
}, [messages, setMessages]);
return messages;
}
export default function App() {
const [message, setMessage] = useState('');
const messages = useMessages();
const [isConnected, setIsConnected] = useState(client.isConnected());
useEffect(() => {
return client.onStateChange(setIsConnected);
}, [setIsConnected]);
useEffect(() => {
if (isConnected) {
client.getClient().send('hi');
}
}, [isConnected]);
function sendMessage(e) {
e.preventDefault();
client.getClient().send(message);
setMessage('');
}
return (
<div>
<h1>Websocket {isConnected ? 'Connected' : 'Disconnected'}</h1>
<form onSubmit={sendMessage}>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">Send</button>
</form>
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
Here is what I use:
const [status, setStatus] = useState('closing')
let socket
useEffect(() => {
if (!condition1) {
return
}
if (socketStatus == 'closing') {
connectSocket()
setSocketStatus('opening')
}
}, [socketStatus])
function connectSocket() {
socket = new WebSocket('ws://...');
socket.addEventListener('open', function (m) {
newSocket.send('...')
});
socket.onmessage = function (e) {
log(e.data)
}
socket.onclose = function (e) {
setTimeout(() => {
setSocketStatus('closing')
}, 2000);
};
socket.onerror = function (err: any) {
socket.close();
};
}

React Sequential Rendering Hook

I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};

Resources