I am getting data from firebase in react, but I am not able to pass on that data as the variables are defined internally. Following is what I am trying to do.
function getCommentNums(item){
const formRef = database.ref(
`/comments/${form_id}/${instanceId}/${item.id}`
);
console.log('formref = ', formRef)
formRef.on('value', async(snap)=>{
const commentsArr = (await snap.val()) ?? [];
console.log('commentArr=', commentsArr.length)
setCommentLen(commentsArr.length)
})
return someNum
}
then in main return statement getcommentnums is called inside accordion
{questions.map((item, index) => (
<Accordion
key={index}
id={
"question-" +
(noOfQuestionsPerPage * (page - 1) + 1 + index)
}
question={item}
questionNo={noOfQuestionsPerPage * (page - 1) + 1 + index}
//match vs item.id
commentNums = {getCommentNums(item)}
onBlur={handleClickSave}
onClickComments={onClickComments}
onChangeAnswer={onChangeAnswer}
answers={answers}
onClickLastValue={onClickLastValue}
disabled={form.is_submitted}
/>
))}
I am trying someNum to be commentsArr.length, which is supposed to be some integer. This function is going to be called in some child component to display value of commentNums. Multiple child components are going to be on one page and each would be calling above fn to get there respective commentNums.
I have tried using set state, but that just causes infinite loop.
Can someone show me how to send commentArr.length value forward?
While you call setCommentLen(commentsArr.length) to update the commentLen state variable, your rendering code still tries to render the return value of getCommentNums, which won't work.
The proper way to implement this is to:
Modify your loader function to no longer return any value, and only update the state.
function loadCommentCount(item){
const formRef = database.ref(`/comments/${form_id}/${instanceId}/${item.id}`);
formRef.on('value', async(snap)=>{
const commentsArr = (await snap.val()) ?? [];
setCommentLen(commentsArr.length)
})
}
Call this loader function outside of the rendering code, for example when the component is created, typically in a useState handler.
useState(() => {
questions.map((item, index) => (
loadCommentCount(item);
})
}, [questions])
Then render the value from the state.
commentNums = {commentCount}
Also see:
React Not Updating Render After SetState
How to return a value from Firebase to a react component?
Firebase response is too slow
My firebase realtime push key is changed when I save it in array in react native
React-native prevent functions from executing asynchronously?
Related
I'm writing a table component for my page in React.
I have a function loadData() that makes a request to an api. Using the api result to update the data state variable, using a new reference.
The problem here is that React doesn't trigger any re-render for the data variable.
const [data, setData] = useState([]);
const loadData = async () => {
try {
...
let response_json = await response.json();
setData(transformData(response_json.items));
...
}
const transformData = (data) => {
if (data === undefined || data === null) {
return [];
}
let new_data = [];
data.forEach((entry,index) => {
new_data.push(cloneElement(props.config.table_entry,{data:entry, key:index}, null));
});
return new_data;
}
I am using this code to change the table's page, making a request with parameters like pageNumber, pageSize, filters. So even with different data and different reference, still it doesn't trigger re-rendering.
This problem has challenged me for like one whole morning was that the data variable continued to updated on every request made but the webpage never re-rendered.
The answer lies here
data.forEach((entry,index) => {
new_data.push(cloneElement(props.config.table_entry,{data:entry, key:index}, null));
});
in the transformData function where it creates a new array of new data, BUT the key property of the component never changed because it was the index of its position in the array returned from the server.
Assigning the key to a unique id solved the problem.
I am showing a list of users(profiles), and fetch it from some users DB.
I am in the search page which include sub pages for diffrenet filters - like which users are currently online.
Each time i am moving inside the search sub pages, i have to reset only once the main filtering variable in order ot get the correct result.
The problem is the fetch request happpend before the setState variable changed.
I saw other people asked how to fetch after, while i need it to first reset the variables of setState and the to go and fetch according to the correct values.
code:
const [isPopUpShowState,setIsPopUpShowState] = useState(false);
const [profilesloading,setProfilesLoading] = useState(<Spinner/>);
const [profilesLength,setProfilesLength] = useState(0);
const [profilesPerPage] = useState(4);
const [searchStartPoint,setSearchStartPoint] = useState(0);
const [lastUserConnIndex,setLastUserConnIndex] = useState(1);
useEffect( ()=> {
restoreStatesToDefault(); // reset states+list --> the variables doesnt changed before the the fetch
getProfilesMatchingPage(); // init profiles
},[history.location.pathname]);
const restoreStatesToDefault = () => {
list = {};
setSearchStartPoint(0);
setLastUserConnIndex(1);
setProfilesLength(0);
}
const getSearchProfilesParmsInObj = () => {
const parmsObj = {};
if(currUser.loginObj){
parmsObj['isMale'] = !currUser.loginObj.data.isMale;
parmsObj['profilesPerPage'] = profilesPerPage;
parmsObj['searchStartPoint'] = searchStartPoint;
parmsObj['lastUserConnIndex'] = lastUserConnIndex;
parmsObj['allProfiles'] = list;
}
return parmsObj;
}
const getProfilesMatchingPage = () => {
switch(history.location.pathname){
case '/search/online':
dispatch(getProfilesOnline(getSearchProfilesParmsInObj(),setProfilesLoading,setLastUserConnIndex,setProfilesLength));
break;
case '/search/pics':
dispatch(getProfilesOnlyWithPics(getSearchProfilesParmsInObj(),setProfilesLoading,setLastUserConnIndex,setSearchStartPoint,setProfilesLength));
break;
case '/search/recently':
dispatch(getProfilesRecentlyVisited(getSearchProfilesParmsInObj(),setProfilesLoading,setLastUserConnIndex,setSearchStartPoint,setProfilesLength));
break;
case '/search/news':
dispatch(getProfilesNewUsersRegistered(getSearchProfilesParmsInObj(),setProfilesLoading,setLastUserConnIndex,setSearchStartPoint,setProfilesLength));
}
}
The problem is that both functions are called within the same lifecycle of the function, so the states haven't updated yet (They are within the same closure). After your useEffect finishes, then the next render is called with the updated state values, but they are not dependencies of your useEffect so they don't trigger it to fire again (which is a good thing in this case).
Basically what you want is two useEffect -> one is triggered on path change, and that one should update state that is a dependency of another useEffect that triggers the fetch.
A simple example would be:
const [shouldFetch, setShouldFetch] = useState(false) // Set this to true if you want to fetch on initial render
useEffect( ()=> {
restoreStatesToDefault(); // reset states+list --> the variables doesnt changed before the the fetch
setShouldFetch(true);
},[history.location.pathname]);
useEffect(() => {
if (shouldFetch) {
setShouldFetch(false);
getProfilesMatchingPage(); // init profiles
}
}, [shouldFetch])
I have a Dropzone that allows for multiple concurrent uploads and I want to show the progress for all uploads.
In my Dropzone component I have part of the state which is an array of uploads:
const [uploads, setUploads] = useState([])
Each element of the uploads array will be an upload object that has a few values, like so:
const uploading = {
id: 1,
files: <array of files>,
progress: 0
}
Once files/folders are dropped into the dropzone, the "uploading" object will be added to the "uploads state array" and the files will be sent to the backend API, which asynchronously uploads the files to the server.
The backend will periodically send a progress callback to the UI, to update the progress value of the correct element in the uploads state array (see progressCallback below)
What I am currently unable to achieve is to make sure the UI re-renders every time an object in the uploads array is being updated to show progress, so that I can show the progress of all uploads as they happen.
The UI Component looks like this:
export function UploaderDropzone(props) {
const [uploads, setUploads] = useState([])
const progressCallback = useCallback((progressObject, sessionContext, step) => {
const {uploadSessionParameters} = sessionContext
let uploading = {}
// let tmpArray = []
const tmpArray = [...uploads]
if (step === 'progress') {
const filtered = findUploadById(tmpArray, uploadSessionParameters.uploadSessionId)
uploading = filtered[0]
if (uploading) {
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
// setUploads(prevUploads => [...prevUploads, uploading])
}
console.log('progress tmpArray = ' + JSON.stringify(tmpArray));
console.log('progress uploads = ' + JSON.stringify(uploads))
}
if (step === 'initialize') {
const uploadNumber = uploads.length + 1
uploading = {
uploadSessionId: uploadSessionParameters.uploadSessionId,
files: sessionContext.files,
uploadNumber: uploadNumber,
uploadName: `Upload #${uploadNumber}`,
sent: false,
progress: 0,
}
tmpArray.push(uploading)
setUploads(tmpArray)
console.log('initialize tmpArray = ' + JSON.stringify(tmpArray))
console.log('initialize uploads = ' + JSON.stringify(uploads))
}
}, [uploads])
const progressBars = uploads.map((upload) => {
return (
<Fragment>
<ProgessBar progress={upload.progress} />
</Fragment>
)
})
// ... more code here ... not required for understanding
return {
<Fragment>
<Dropzone
onDrop={
acceptedFiles => {
const filteredFiles = acceptedFiles.filter((file) =>
validateFile(file))
console.log("Filtered files" + filteredFiles)
if (filteredFiles.length > 0) {
setAlertMsg('')
}
else {
setAlertMsg('No files uploaded.')
}
// call to Node.js backend, passing it the progressCallback
startAsyncUploadSession(filteredFiles, progressCallback);
}
}
/>
{progressBars}
</Fragment>
}
}
The ProgressBar component is very simple:
export function ProgressBar(props) {
const {progress} = props
return (
<Fragment>
<p>`${progress}% uploaded ...`</p>
</Fragment>
)
}
Right now, this code doesn't even show the progress bar even though the uploads state array is constantly being updated in the progressCallback. Since I don't know the number of concurrent uploads that will be done, I cannot set the state in the higher order component and pass it as props, I need the child component (ProgressBar) to receive it as props from the multiple objects in the state array ... but I am clearly missing something ...
Any pointers ? Any hooks I could use to register the progress value of the objects in the uploads state array so that every time the backend updates us on the progress it is reflected in the UI ?
Edit: To include the partial fix suggested by #Robin Zigmond
Edit2: After some debugging, it seems to be a synchronization issue. So I need to add some code and details here.
When files are dropped into the Dropzone, its sends the files to the Node.js backend through a function call to a mock server, the call to startAsyncUploadSession(filteredFiles, progressCallback); in the onDrop event of the Dropzone (which uses the react-dropzone lib).
It would seem that when I call progressCallback later, the state is as it was on the first render, aka uploads state array is an empty array as it was when the files were dropped, not the updated one which contains the object added to uploads array in the 'initializing' step.
So amended question would be "How to make sure that the UI state is up to date when the progressCallback is called later on by the backend ?"
The problem is in your state updating code inside progressCallback. Here is the offending code, for reference:
const tmpArray = uploads
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
// HERE UPDATING ONE OF ITEM'S VALUES IN UPLOADS STATE ARRAY
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
What this does is:
sets tmpArray to be a reference to the same object (uploads) as the current state
then mutates that array, first by splicing an element out, then pushing a new element on to it
At no point in step 2) does the reference change. So when you then call setUploads(tmpArray) - which might as well be setUploads(uploads) as those two variables are still references to the exact same array - React thinks you're setting the state to be exactly what it was, and therefore doesn't know to rerender.
That's the long way of explaining why you should never mutate state, as you are doing here. You need to update it immutably - that is, leave the old state alone, construct a new object/array, and pass that to the function that sets the new state.
There are a number of ways to do that here, but in your case it should be as simple as just making tmpArray a (shallow) *copy) of the current state. That is, change:
const tmpArray = uploads
to
const tmpArray = [...uploads]
Now tmpArray is a new reference, to an array holding the same values as before. Note that the copy is only "shallow", so the objects inside the array are still references to just one underlying object for each array element. But that doesn't seem to matter here, because you don't mutate those objects. If you try your code with this change, I believe it should start to work (or at least get you past this particular problem).
Quick version:
My ultimate goal is to do something like the link below but with an async call to firebase per useEffect where the list data is composed of firebase object content.
https://codesandbox.io/s/usage-pxfy7
Problem
In the code below useEffect encapsulates code that pings firebase and gets some data back called "clients". The data is retrieved perfectly.
I then store that data using useState to two different instances of useState. The data is stored at clientList and clientListForRender.
So far so good.
Now the problem starts.
I have a third instance of useState that takes a number. I want to set a keypress event to the document so that I can use the up/down arrows to toggle the counter and access each value of the clientListForRender array.
When I set the eventListener I do not have access to the array (presumably due to the async calls not being in an order that allows for it).
I am not sure how to write my hooks in a way that gives me the result I want.
Thank you.
const clientsRef = firebase.database().ref('clients');
const [clientList,setClientListState] = useState([]);
const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event,arr){
console.log(arr)
if(event.key === "ArrowDown"){
updateSelectedIndex((prev)=>{
return prev += 1
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev){
setClientListStateForRender(()=>[client,...prev]); //_______2 store data
// document.addEventListener('keydown', handleKeyPress); <---I am not sure where to put this. I have experimented and
// I decided to omit my cluttered "experiments" to protect your eyes
return[client,...prev]
});
});
},[]);
Ok there are few issues with the code you posted:
1) You should definitely not add your keyboard listener in the child_ added listener ( this means that every time the child_added listener is called, you are going to create a new listener, leading to unexpected results and memory leak)
2) You are calling setState in a setState updater function (the callback function you provided for, setClientListState), which is an anti pattern and makes your code hard to follow and understand, and will cause unexpected effects once the component grows. If you want to update a state based on a previous state then use the useEffect callback
3) the useEffect function takes a second parameter, called array of dependencies. When you have provided it with an empty array, it means that you want your effect to run only once, which is problematic because we see that the function depends on clientsRef variable. ( from this actually comes your problem because the keyboard listener was having the old value of your clientsList which is the empty array, and so it was always returning 0, when keys where pressed, i explained more in the code sandbox)
4)You should return a callback function from the useEffect function to clean the effects you created, turning off the listeners you attached (or else you might have memory leaks depending on how much the component gets mounted/unmounted)
ok here is how the code should be to work:
const clientsRef = firebase.database().ref('clients');
const [clientList, setClientListState] = useState([]);
// I don't understand why you wanted another list, so for now i only use on list
// const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event, arr) {
if (event.key === 'ArrowDown') {
updateSelectedIndex(prev => {
if (prev >= clientList.length - 1) {
return (prev = 0);
} else {
return prev + 1;
}
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev) {
return [client, ...prev];
});
});
document.addEventListener('keydown', handleKeyPress);
// here you should return a callback to clear/clean your effects
return () => {
document.removeEventListener('keydown', handleKeyPress);
clientsRef.off();
};
// Its important to add these here, or else each time your keyboard listener runs it will have the initial value of
// clientsList ([]), and so clientsList.length = 0, and so you will always updateSelectedIndex(0)
}, [clientList, clientsRef]);
//here render based on selected list as you wish
Finally i have set up a working codesandbox that emulated data fetching based on the example you give https://codesandbox.io/s/usage-4sn92, i added some comments there to help explain what i said above.
I recently started with react (alongside react-native) and a lot of things are alien to me. I was creating an app in react native which shows cryptocurrency value in real time.
Coin cap provides web-socket connection so that the new values are automatically updated instead of fetching the entire stuff using axios.
Now, In simple HTML and javascript world, I assigned an ID to the elements and used for loop whenever the new data came from web-socket to find the value I need to update(or change).
Something like this
var socket = io.connect('https://coincap.io');
socket.on('trades', function (tradeMsg) {
var crypto = tradeMsg.coin;
for (let i=0; i<500; i++) {
var compare = cryptoSName[i].innerHTML;
if (compare == crypto) {
var oldPrice = document.getElementById('price' + i).innerHTML;
document.getElementById('price' + i).innerHTML= (tradeMsg.message.msg.price.toFixed(4));;
document.getElementById('perc' + i).innerHTML= tradeMsg.message.msg.perc + "%";
document.getElementById('pricer1' + i).innerHTML= (tradeMsg.message.msg.price.toFixed(4));;
document.getElementById('percr1' + i).innerHTML= tradeMsg.message.msg.perc + "%";
var newPrice = tradeMsg.message.msg.price;
[Question] In react I can still assign an ID to the elements but how can we do something like that (using web-socket to update data)? Also, keeping in mind how react components render (or re-render) things.
[Update] I am using Redux which stores the data in a state. Consider this
Data/state I receive from redux (through axios api call in some action)
class cryptoTicker extends Component {
componentWillMount() {
this.props.fetchCoin();
}
render() {
var CryptoData = this.props.cryptoLoaded;
let displayCrypto = CryptoData.map(el => {
return (
<CoinCard
key={el["long"]}
coinPrice = {el["price"].toFixed(2)}
/>
);
});
return (
<ScrollView>{displayCrypto}</ScrollView>
);
}
}
const mapStateToProps = state => {
return {
cryptoLoaded: state.posts.itemsSucess
}
};
export default connect(mapStateToProps, {fetchCoin})(cryptoTicker);
Note: Initially all the data is fetched through axios (ajayx) and websocket only sends changes in the fetched data
Just in case: Here is the link to coincap api's documentation
https://github.com/CoinCapDev/CoinCap.io