I'm using SSE to get a stream of strings, which I need to store in a useState to display it word by word. However, after I have changed the state and the component rerenders, the old state is retained OUTSIDE the eventSource.onmessage function. As soon as it comes inside it, the state gets the default value.
const [currentResp, setCurrentResp] = useState('');
const [respHistory, setRespHistory] = useState({
count: 0,
queryName: null,
queryData: [],
});
console.log('currentResp after it has been changed', currentResp);
const toggleRequest = () => {
if (query == respHistory.queryName) {
console.log('asd');
} else {
setRespHistory({
count: 1,
queryName: query,
...respHistory,
});
}
const url = 'https://somesseurl/sse';
const sse = new EventSource(url);
function getRealtimeData(data) {
console.log('Inside Function currentResp - ', currentResp);
const currentSSEText = data.choices[0].text;
setCurrentResp(currentResp + currentSSEText);
}
sse.onmessage = (e) => getRealtimeData(JSON.parse(e.data));
sse.onerror = (error) => {
console.log(error);
sse.close();
};
return () => {
sse.close();
console.log('Closed');
};
};
I want to retain the old state even inside the function, such that I keep concatinating the string stream that is coming from the SSE.
It is happening because you are using the current state reference to update the state.
setCurrentResp(currentResp + currentSSEText);
In react, the state updates are asynchronous, so when you update any state react puts that state update inside a queue, and then react will execute the state updates one by one from the queue and the state will be updated.
So when you're trying to update the state using the currentResp as the previous state value, it may not work because currentResp may not have the latest value.
So if you want to access the previous state value inside a state update, you can do it like below
setCurrentResp((prev) => {
return prev + currentSSEText
});
Related
I have an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.
My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once
I'm quite new to the React-TS world and I have recently been playing with useState and useEffect hooks only basically.
I have the following functional component inside which I'd like to fetch N items the first time and then start a periodic function that fetches the last item from the response data, updating the current state.
const fetcher = async (url: string) => await axios.get(url).then((res: AxiosResponse) => res.data);
type AirflowData = {
value: number; // perc values from 0 to 1
timestamp: number; // UTC time
};
const ActionDetector: React.FC = () => {
const [alerts, setAlerts] = useState<AirflowData[]>([]);
useEffect(() => {
// Fetch the latest N alerts first
getAlerts(100);
// Then start fetching the last alert every N milliseconds
const interval = setInterval(() => getLatestAlert(), 1000);
// Clear interval
return () => {
clearInterval(interval);
};
}, []);
/**
* Return the alert data after fetching it.
* #param numAlerts number of the last N alerts to return
*/
const getAlerts = async (numAlerts: number) => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
setAlerts(fetchedAlerts.slice(-numAlerts));
};
/**
* Return the latest alert data available.
*/
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
setAlerts([...alerts, latestFetchedAlert]);
}
};
console.log(alerts);
return <></>
}
export default ActionDetector
The problem with this approach is that latestAlert is always undefined and that is due, if I understood how React works under the hood correctly, to the initial state change re-rendering trigger. After getAlerts() is called and fires setAlerts(...), the component starts the re-rendering and so, since getLatestAlert() is called inside the useEffect only the first time (the first render), it always read alerts as the initialized empty array.
I don't know if this is the correct reason behind this, but how can I achieve what I'm trying to do the right way?
The fundamental issue is that when updating state based on existing state, you need to be sure you have the latest state information. Your getLatestAlerts function closes over the alerts constant that was in scope when it was created, so it only ever uses that version of the constant (not the updated one from a subsequent render). Your useEffect setInterval callback closes over the getLatestAlerts function that was in scope when it was created, and only ever uses that version of the function.
To be sure you have the latest state, use the callback version of the state setter instead of the constant:
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
if (latestFetchedAlert) {
setAlerts(alerts => {
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
alerts = [...alerts, latestFetchedAlert];
}
return alerts;
});
}
};
Purely as a side note, I wouldn't use the idiom you seem to be using to get the last item from an array, array.slice(-1)[0]. Instead, I'd either use array[array.length - 1], or use the at method which just achieved Stage 4 and will be in this year's spec (it's easily polyfilled for older environments).
I am newbie in React Native and I am trying to store and get an array with AsyncStorage in ReactNative.
I have two problems.
First, I do not know why but when I storage data, it only works the second time but I am calling first the set of useState.
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
Second, how can I call the getData function to get all the data and show it? Are there something like .onInit, .onInitialize... in ReactNative? Here is my full code
const [task, setTask] = useState();
const [taskItems, setTaskItems] = useState([]);
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
const completeTask = (index) => {
var itemsCopy = [...taskItems];
itemsCopy.splice(index, 1);
setTaskItems(itemsCopy);
storeData(taskItems);
}
const storeData = async (value) => {
try {
await AsyncStorage.setItem('#tasks', JSON.stringify(value))
console.log('store', JSON.stringify(taskItems));
} catch (e) {
console.log('error');
}
}
const getData = async () => {
try {
const value = await AsyncStorage.getItem('#tasks')
if(value !== null) {
console.log('get', JSON.parse(value));
}
} catch(e) {
console.log('error get');
}
}
Updating state in React is not super intuitive. It's not asynchronous, and can't be awaited. However, it's not done immediately, either - it gets put into a queue which React optimizes according to its own spec.
That's why BYIRINGIRO Emmanuel's answer is correct, and is the easiest way to work with state inside functions. If you have a state update you need to pass to more than one place, set it to a variable inside your function, and use that.
If you need to react to state updates inside your component, use the useEffect hook, and add the state variable to its dependency array. The function in your useEffect will then run whenever the state variable changes.
Even if you're update state setTaskItems([...taskItems, task]) before save new data in local storage, storeData(taskItems) executed before state updated and save old state data.
Refactor handleAddTask as below.
const handleAddTask = () => {
Keyboard.dismiss();
const newTaskItems = [...taskItems, task]
setTaskItems(newTaskItems);
storeData(newTaskItems);
};
I started learning react, yesterday I ran into this issue, somebody please explain me.
When I click button "add to wishlist" the piece of code below run 1st time, set product property "inWishList": true, but unknown why it rerun and set it back to "false" value.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList;
console.log(latest_Products);
return latest_Products;
});
};
_ The piece of code below works perfect, run only 1 time, however, i don't understand the difference between 2 codes
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList];
updatedProducts[prodIndex] = {
...currentProdList[prodIndex],
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
In the first snippet you are mutating the state object when toggling the inWishList property.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList; // <-- state mutation
console.log(latest_Products);
return latest_Products;
});
};
The second snippet you not only shallow copy the previous state, but you also shallow copy the element you are updating the inWishList property of.
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList]; // <-- shallow copy state
updatedProducts[prodIndex] = {
...currentProdList[prodIndex], // <-- shallow copy element
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
Now the reason these two code snippets function differently is likely due to rendering your app into a StrictMode component.
StrictMode
Specifically in reference to detecting unexpected side effects.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
When you pass a function to setProducts react actually invokes this twice. This is exposing the mutation in the first example while the second example basically runs the same update twice from the unmutated state, so the result is what you expect.