Perform useState before next setInterval call - reactjs

I'm working on a console component that retrieves logs from a server. To prevent the console from being overcrowded, I implemented a function that clears the console output. However, the clearing is overwritten by the next setInterval call.
const [logs, setLogs] = useState([])
const saveLogs = (newLogs) => {
setInterval(() => setLogs(newLogs), 2000);
};
const clearConsole = () => {
setLogs([])
};
logs is an array of strings and I'm appending a new log every time I receive a new log from the server.
Using clearInterval() before updating the state would solve the problem, but as I intend to use multiple tabs for the console, I would have to clear the interval of every instance, which is not what I want.

Related

How do I asynchronously update a variable from a paginated API using React hooks?

I'm currently trying to fetch all of the properties for an object from an API, and display them in a table. The API will return up to 10 results at a time, and will return a value nextPageToken in the response body if there are more results to be fetched. My goal is to fetch the first 10 results, immediately display them in the table, and add to the table as I continue to hit the API. This was my first attempt at a solution:
const getProperties = async (id) => {
const properties = await Api.getProperties(id);
setProperties(properties.properties);
if (properties.nextPageToken) loadMoreProperties(id, nextPageToken);
};
const loadMoreProperties = async (id, nextPageToken) => {
const properties = await Api.getProperties(id, nextPageToken);
setProperties(prevProperties => {return [...prevProperties, properties.properties]});
if (properties.nextPageToken) loadMoreProperties(id, properties.nextPageToken);
};
(Note that the above is a simplification; in practice, there's more logic in getProperties that doesn't need to be repeated on subsequent calls to the API)
The problem that I'm running into with this solution is that when I'm calling loadMoreProperties, the setProperties call isn't yet finished. How can I enforce that the call to loadMoreProperties only happens after setting the previous set of properties? Is there an overall better pattern that I can follow to solve this problem?
You can use useEffect to trigger the page loads as a reaction to a completed state change:
const [page, setPage] = useState(); // will be {properties, nextPageToken}
// load first page whenever the id changes
useEffect(() => {
Api.getProperties(id)
.then(page => setPage(page)));
}, [id]);
// load next page (if there is one) - but only after the state changes were processed
useEffect(() => {
if (page?.nextPageToken == null) return;
Api.getProperties(id, page.nextPageToken)
.then(page => setPage(page)));
}, [id, page]
);
// this will trigger the re-render with every newly loaded page
useEffect(()=> setProperties(prev => [...(prev || []), page.properties]), [page]);
The first effect will cause an update to the state variable page.
Only after the state change is completed, the second effect will be triggered and initiate the fetch of the second page.
In parallel, the third effect will perform the changes to the state variable properties, that your table component depends on, after each successful page load and page state update, triggering a re-render after each update.
I think you should pass a callback parameter to your "setProperties" method, to make the second call after the value has been updated, like this :
setProperties(properties.properties, () => {
if (properties.nextPageToken)
loadMoreProperties(id, nextPageToken);
);
Hope it can help
My solution involves removing the loadMoreProperties method itself.
While calling the getProperties for the 1st time, you can omit the nextPageToken argument.
getProperties = async(id,nextPageToken) {
var result = await Api.getProperties(id,nextPageToken);
this.setState((state)=>(state.properties.concat(result.properties)), ()=>{
// setState callback
if(result.nextPageToken) {
this.getProperties(id, nextPageToken);
}
});
}

Firebase firestore read operation very high

So basically im making a CRUD app using react and firebase firestore for the backend.
My write and delete operation is doing well, there is no problem with it.
But my read operation have problem.
My web is getting all document from a collection in firebase using useEffect. So this only run whenever it first mount (when my web load first time) and when im changing "users" value when doing delete and create operation
this my code:
useEffect(() => {
const getUsers = async () => {
const querySnapshot = await getDocs(collection(db, "cobadata"));
setUsers(querySnapshot.docs.map((doc)=> ({...doc.data(), id: doc.id})))
};
getUsers();
}, [users]);
idk whats wrong but im getting a very high read operation when im test the web, its like every one read operation i do in my website, its getting like hundred operation in the firebase. i can see this in my firebase console, when im using the web just like 5 minute in my firebase console the read operation reaching 20k< operation.
can anyone help me how to deal with this, thanks!
You dont show all of your code here, so I will need to do some guessing.
Your useEffect has a dependency array that now is set to [users]. This means that every time the variable users changes your useEffect will rerender. Inside your useEffect you then set a new value to users by the setUsers function. Even if you get the same values returned from firebase regarding the current users, you still create a new array each time you read data. (querySnapshot.docs.map((doc)=> ({...doc.data(), id: doc.id}))). React only does a shallow comparison, meaning that the object reference has changed, and therefore users is different on each render.
First you need to decide when you want to run the useEffect and what should trigger it. If changes in the variable users is not the correct place to check, then I would remove users from the dependency array.
One solution could be to move the functionality in your effect into its own function and wrap it in an useCallbac. You can then call this function from an ´useEffect` on initial load, and after that simply load the effect whenever you delete or create users. Something like this.
const getUsers = useCallback(async () => {
const querySnapshot = await getDocs(collection(db, "cobadata"));
setUsers(querySnapshot.docs.map((doc)=> ({...doc.data(), id: doc.id})))
}, [collection])
useEffect(() => {
getUsers()
}, [getUsers]);
const createUser = () => {
...
getUsers()
}
const deleteUser = () => {
...
getUsers()
}
(PS! I would recommend adding the eslint-plugin-react-hooks to your eslint-config. This will give you some warning if your hooks are used wrong)

Set state on changed dependencies without additional render (having a loader without additional renders)

I have a huge component which renders long enough to be noticeable by user if it happens too often.
Its contents are loaded asynchronously (from a server) every time when a function "getData" changes, showing a loader during the wait.
I'm trying to write a code which will render the component only 2 times when the function changes - first time to show the loaded and the second time to display the data.
Using a standard useEffect causes it to be rendered 3 times, first of which doesn't change anything visible for the user.
type tData = /*some type*/
const Component = (props: {getData: () => Promise<tData[]>}) => {
const {getData} = props;
const [loader, setLoader] = useState(true);
const [data, setData] = useState<tData[]>([]);
useEffect(() => {
setLoader(true);
setData([]);
getData().then((newData) => {
setData(newData);
setLoader(false);
});
}, [getData, setLoader, setData]);
// ... the rest of the component (that doesn't use the function getData)
};
The three renders are:
getData has changed - the effect runs and changes the states but there is 0 changes visible to the user (this is the render I want to get rid of)
loader has changed to true and data has changed to [] - a useful render that actually changes the UI
loader has changed to false and data has changed to a new value - a useful render that actually changes the UI
How could I modify this code to not have a barren render when getData changes?
this is a very common use case.
what you can do here is a if() in the useEffect().
using a .then() complicates things. use async await to make it asynchronus.
useEffect(() => {
async function loadData() {
const res = getData()
return res
}
let data = await loadData()
//check if it's undefined
if (data && data!=='init') {
setStateData(data)
}
}, [])
this is how i'd implement something like this. however it seems like the data fetching for you is somewhat complicated then this, but essentially, never fetch data synchronously unless you absolutely have to, and check if it's undefined in useEffect before setting the state variable so that it's only set once.

Infinite Loop with useEffect - ReactJS

I have a problem when using the useEffect hook, it is generating an infinite loop.
I have a list that is loaded as soon as the page is assembled and should also be updated when a new record is found in "developers" state.
See the code:
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = async () => {
await api.get('/developers').then(response => {
setDevelopers(response.data);
});
};
// This way, the loop does not happen
useEffect(() => {
getDevelopers();
}, []);
// This way, infinte loop
useEffect(() => {
getDevelopers();
}, [developers]);
console.log(developers)
If I remove the developer dependency on the second parameter of useEffect, the loop does not happen, however, the list is not updated when a new record is found. If I insert "developers" in the second parameter of useEffect, the list is updated automatically, however, it goes into an infinite loop.
What am I doing wrong?
complete code (with component): https://gist.github.com/fredarend/c571d2b2fd88c734997a757bac6ab766
Print:
The dependencies for useEffect use reference equality, not deep equality. (If you need deep equality comparison for some reason, take a look at use-deep-compare-effect.)
The API call always returns a new array object, so its reference/identity is not the same as it was earlier, triggering useEffect to fire the effect again, etc.
Given that nothing else ever calls setDevelopers, i.e. there's no way for developers to change unless it was from the API call triggered by the effect, there's really no actual need to have developers as a dependency to useEffect; you can just have an empty array as deps: useEffect(() => ..., []). The effect will only be called exactly once.
EDIT: Following the comment clarification,
I register a developer in the form on the left [...] I would like the list to be updated as soon as a new dev is registered.
This is one way to do things:
The idea here is that developers is only ever automatically loaded on component mount. When the user adds a new developer via the AddDeveloperForm, we opportunistically update the local developers state while we're posting the new developer to the backend. Whether or not posting fails, we reload the list from the backend to ensure we have the freshest real state.
const DevList: React.FC = () => {
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = useCallback(async () => {
await api.get("/developers").then((response) => {
setDevelopers(response.data);
});
}, [setDevelopers]);
useEffect(() => {
getDevelopers();
}, [getDevelopers]);
const onAddDeveloper = useCallback(
async (newDeveloper) => {
const newDevelopers = developers.concat([newDeveloper]);
setDevelopers(newDevelopers);
try {
await postNewDeveloperToAPI(newDeveloper); // TODO: Implement me
} catch (e) {
alert("Oops, failed posting developer information...");
}
getDevelopers();
},
[developers],
);
return (
<>
<AddDeveloperForm onAddDeveloper={onAddDeveloper} />
<DeveloperList developers={developers} />
</>
);
};
The problem is that your getDevelopers function, calls your setDevelopers function, which updates your developers variable. When your developers variable is updated, it triggers the useEffect function
useEffect(() => {
getDevelopers();
}, [developers]);
because developers is one of the dependencies passed to it and the process starts over.
Every time a variable within the array, which is passed as the second argument to useEffect, gets updated, the useEffect function gets triggered
Use an empty array [] in the second parameter of the useEffect.
This causes the code inside to run only on mount of the parent component.
useEffect(() => {
getDevelopers();
}, []);

Graphql subscriptions inside a useEffect hook doesn't access latest state

I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},

Resources