Disclaimer: Please don't mark this as duplicate. I've seen similar questions with answers. But none of them is working for me. I'm just learning React.
What I'm trying to achieve is basically infinite scrolling. So that when a user scrolls to the end of the page, more data will load.
I've used scroll eventListener to achieve this. And it is working.
But I'm facing problems with the state of the variables.
First, I've changed the loading state to true. Then fetch data and set the state to false.
Second, when scrolling to the end of the page occurs, I again change the loading state to true. Add 1 with pageNo. Then again fetch data and set the loading state to false.
The problems are:
loading state somehow remains true.
Changing the pageNo state is not working. pageNo always remains to 1.
And actually none of the states are working as expected.
My goal: (Sequential)
Set loading to true.
Fetch 10 posts from API after component initialization.
Set loading to false.
After the user scrolls end of the page, add 1 with pageNo.
Repeat Step 1 to Step 3 until all posts loaded.
After getting an empty response from API set allPostsLoaded to true.
What I've tried:
I've tried adding all the states into dependencyList array of useEffect hook. But then an infinite loop occurs.
I've also tried adding only pageNo and loading state to the array, but same infinite loop occurs.
Source:
import React, { lazy, useState } from 'react';
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
const defaultPosts: BlogPost[] = [{
Id: 'asdfg',
Content: 'Hi, this is demo content',
Title: 'Demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home = (props: any) => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function handleScroll(event: any) {
console.log('loading ' + loading);
console.log('allPostsLoaded ' + allPostsLoaded);
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setLoading(true);
setPageNo(pageNo => pageNo + 1);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
})
}, 1000);
}
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
init();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;
Used more effects to handle the change of pageNo, loader and allPostsLoaded state worked for me.
Updated Source:
import React, { lazy, useState } from 'react';
import { Guid } from "guid-typescript";
import { PostSection } from './Home.styles';
import { BlogPost } from '../../models/BlogPost';
import { PostService } from '../../services/PostService';
import { Skeleton } from 'antd';
const defaultPosts: BlogPost[] = [{
Id: '456858568568568',
Content: 'Hi, this is demo content. There could have been much more content.',
Title: 'This is a demo title',
sections: [],
subTitle: '',
ReadTime: 1,
CreatedDate: new Date()
}];
const defaultPageNo = 1;
const defaultPageSize = 10;
const PostCardComponent = lazy(() => import('./../PostCard/PostCard'));
const postService = new PostService();
const Home: React.FC<any> = props => {
const [posts, setPosts]: [BlogPost[], (posts: BlogPost[]) => void] = useState(defaultPosts);
const [pageNo, setPageNo] = useState(defaultPageNo);
const [pageSize, setPageSize] = useState(defaultPageSize);
const [loading, setLoading] = useState(false);
const [allPostsLoaded, setAllPostsLoaded] = useState(false);
const [featuredPost, setFeaturedPost]: [BlogPost, (featuredPost: BlogPost) => void] = useState(defaultPosts[0]);
function getNewGuid() {
return Guid.create().toString();
}
async function getPosts() {
return await postService.getPosts(pageSize, pageNo);
}
async function getFeaturedPost() {
return await postService.getFeaturedPost();
}
function init() {
setLoading(true);
Promise.all([getFeaturedPost(), getPosts()])
.then(
responses => {
setLoading(false);
setFeaturedPost(responses[0].data.data);
setPosts(responses[1].data.data);
}
);
}
React.useEffect(() => {
init();
return;
}, []);
React.useEffect(() => {
if (allPostsLoaded || loading) return;
function handleScroll(event: any) {
var target = event.target.scrollingElement;
if (!loading && !allPostsLoaded && target.scrollTop + target.clientHeight === target.scrollHeight) {
setPageNo(pageNo => pageNo+1);
}
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [loading, allPostsLoaded]
);
React.useEffect(() => {
if (pageNo > 1) {
setLoading(true);
setTimeout(()=>{
getPosts()
.then(response => {
const newPosts = response.data.data;
setTimeout(()=>{
setLoading(false);
if (newPosts.length) {
const temp = [ ...posts ];
newPosts.forEach(post => !temp.map(m => m.Id).includes(post.Id) ? temp.push(post) : null);
setPosts(temp);
} else {
setAllPostsLoaded(true);
}
}, 1000);
})
}, 1000);
}
}, [pageNo]
);
return (
<PostSection className="px-3 py-5 p-md-5">
<div className="container">
<div className="item mb-5">
{posts.map(post => (
<PostCardComponent
key={post.Id}
Title={post.Title}
intro={post.Content}
Id={post.Id}
ReadTime={post.ReadTime}
CreatedDate={post.CreatedDate}
/>
))}
</div>
</div>
</PostSection>
);
};
export default Home;
Related
So I wanted to add "whatsapp" like voice note feature in my app I am working on, where you record the voice note and click the "Send" button to send it. I have added the voice recorder code and its working fine in my Logs, but the problem is that when I press the "Send recording" button it sends an empty file in the chat box, and on pressing the same button the second time it then actually sends the recorded Voice note.
The Recorder code component "useRecorder"
import { useEffect, useState } from "react";
import Conversation_Logs from "../Logs/Conversation_Logs";
const useRecorder = () => {
const [audioURL, setAudioURL] = useState("");
const [audioBlob, setAudioBlob] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [recorder, setRecorder] = useState(null);
useEffect(() => {
// Lazily obtain recorder first time we're recording.
if (recorder === null) {
if (isRecording) {
requestRecorder().then(setRecorder, console.error);
}
return;
}
// Manage recorder state.
if (isRecording) {
recorder.start();
} else {
recorder.stop();
}
// Obtain the audio when ready.
const handleData = e => {
setAudioURL(URL.createObjectURL(e.data));
let wavfromblob = new File([e.data], "incomingaudioclip.wav")
setAudioBlob(wavfromblob);
};
recorder.addEventListener("dataavailable", handleData);
return () => recorder.removeEventListener("dataavailable", handleData);
}, [recorder, isRecording]);
const startRecording = () => {
setIsRecording(true);
};
const stopRecording = () => {
setIsRecording(false);
};
return [audioURL,audioBlob, isRecording, startRecording, stopRecording];
};
async function requestRecorder() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return new MediaRecorder(stream);
}
export default useRecorder;
The Code where send recording button is called component named "RecorderBox
import React, {useState,useEffect} from "react";
import { render } from "react-dom";
import useRecorder from "./useRecorder";
const RecorderBox = (props) =>{
const { log, handleUpdateLog,handleSubmitNewMessage ,selectedLog} = props
let [audioURL,audioBlob, isRecording, startRecording, stopRecording] = useRecorder();
const [ newMessage, setNewMessage ] = useState("");
const [ attachFile, setAttachFile ] = useState();
const submitNewMessage = () => {
setAttachFile(audioBlob)
const body = {
body: newMessage,
attachment: attachFile
}
handleUpdateLog(log,newMessage, attachFile)
console.log(attachFile)
// handleSubmitNewMessage(log.id,body)
}
useEffect(() => {
setNewMessage("")
setAttachFile(null)
!!document.getElementsByClassName("dzu-previewButton")[0] && document.getElementsByClassName("dzu-previewButton")[0].click()
}, [log])
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={() => {
stopRecording();
}} disabled={!isRecording}>
stop recording
</button>
<button onClick={() => {
submitNewMessage();
}}>
send recording
</button>
<p>
<em>
</em>
</p>
</div>
);
}
export default RecorderBox;
TL;DR: submitNewMessage is running in the scope of the previous render, thus attachFile doesn't contain anything yet. Whatever audio you managed to send on the second click was actually the first recording you tried to send.
Solution: Skip attachFile completely.
A couple of hints: Don't have a useEffect depend on a state that the effect itself is updating, and dont use a useEffect for doing something that a click-handler can do.
Here's a safer version of your code. I haven't tested it, but any bugs should be easy to fix.
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo<T>(
callback: () => Promise<T> | T,
deps?: any[]
) {
// Private states
const [state, setState] = useState<T>();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState<string>();
const [audioBlob, setAudioBlob] = useState<File>();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
(e: BlobEvent) => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}: any) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
JavaScript version:
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo(callback, deps) {
// Private states
const [state, setState] = useState();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState();
const [audioBlob, setAudioBlob] = useState();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
e => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
I'm having two problems in this code. The first is in the second useEffect. For the reason that I don't understand the useEffect stops working every now and then and causes an error "Cannot read property 'toLowerCase'". Removing the toLowerCase does not solve the problem, but the whole array handling seems to be impossible at that time.
The other problem is in the function addName. setNewName does not set newName. That one I've tried in various kinds of forms, such as setNewName(...newName, {name: '', number: ''}), setNewName('') inside .then and else as well as outside else.
...
import React, {useState, useEffect} from 'react'
import Filter from './components/Filter'
import PersonForm from './components/PersonForm'
import Persons from './components/Persons'
import personService from './services/person'
const App = () => {
const [person, setPerson] = useState([])
const [newName, setNewName] = useState({name: '', number: ''})
const [filteredPerson, setFilteredPerson] = useState([''])
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPerson(initialPersons)
})
}, [])
useEffect( () => {
const results = person.filter( p =>
p.name.toLowerCase().includes(searchTerm) )
setFilteredPerson(results)
},[person,filteredPerson] )
const addName = (event) => {
event.preventDefault()
const nameObject = {
name: newName.name,
number: newName.number
}
if (person.some(p => p.name === newName.name)
) {
window.alert(`${newName.name} is already added to phonebook`)
}
else {
personService
.create(nameObject)
.then(returnedPerson => {
setPerson(person.concat(returnedPerson))
setNewName({name: '', number: ''})
})
console.log('newName', newName.name )
}
}
const handleAddPerson = (event) => {
console.log('event.target.name ', event.target.name)
console.log('event.target.value ', event.target.value)
setNewName({...newName,
[event.target.name]: event.target.value
})
}
const handleSearchTerm = (event) => {
setSearchTerm(event.target.value)
}
return (
<div >
<h2>Phonebook</h2>
<Filter searchTerm={searchTerm} onChange={handleSearchTerm} />
<h3>Add a new</h3>
<PersonForm onSubmit={addName} onChange={handleAddPerson} />
<h2>Numbers</h2>
<Persons list={filteredPerson} />
</div>
);
}
export default App;
...
import axios from 'axios'
const baseUrl = 'http://localhost:3001/persons'
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
const create = newObject => {
const request = axios.post(baseUrl, newObject)
return request.then(response => response.data)
}
const update = (id, newObject) => {
const request = axios.put(`${baseUrl}/${id}`, newObject)
return request.then(response => response.data)
}
/*const updater = {
getAll,
create,
update
}*/
export default {
getAll,
create,
update
}
EDIT
Use async await in your personService so you can return response instead of return request.then(...) something like:
const getAll = async () => {
const response = await axios.get(baseUrl);
return response;
}
After that you can do as follows in your useEffect
useEffect(() => {
(async () => {
const response = await personService.getAll();
if (response.status === 200) {
setPerson(response.data);
const filtered = response.data.filter(item =>
item.name.toLowerCase().includes(searchTerm)
);
setFilteredPerson([...filtered]);
}
})();
}, []);
I am trying to pull data from an Axios Get. The backend is working with another page which is a React component.
In a function however, it doesn't work. The length of the array is not three as it is supposed to be and the contents are empty.
I made sure to await for the axios call to finish but I am not sure what is happening.
import React, { useState, useEffect } from "react";
import { Container } from "#material-ui/core";
import ParticlesBg from "particles-bg";
import "../utils/collagestyles.css";
import { ReactPhotoCollage } from "react-photo-collage";
import NavMenu from "./Menu";
import { useRecoilValue } from "recoil";
import { activeDogAtom } from "./atoms";
import axios from "axios";
var setting = {
width: "300px",
height: ["250px", "170px"],
layout: [1, 3],
photos: [],
showNumOfRemainingPhotos: true,
};
const Collages = () => {
var doggies = [];
//const [dogs, setData] = useState({ dogs: [] });
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
//setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);
return (
<>
<NavMenu />
<ParticlesBg type="circle" margin="20px" bg={true} />
<br></br>
<div>
{doggies.length === 0 ? (
<div>Loading...</div>
) : (
doggies.map((e, i) => {
return <div key={i}>{e.name}</div>;
})
)}
</div>
<Container align="center">
<p> The length of dogs is {doggies.length} </p>
<h1>Knight's Kennel</h1>
<h2> The value of dog is {dog}</h2>
<h2>
Breeders of high quality AKC Miniature Schnauzers in Rhode Island
</h2>
<section>
<ReactPhotoCollage {...setting} />
</section>
</Container>
</>
);
};
export default Collages;
Try doing the following:
const [dogs, setData] = useState([]);
[...]
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
const fetchData = async () => {
getPets();
};
useEffect(() => {
fetchData();
}, []);
No idea if it will actually work, but give it a try if you haven't.
If you don't use useState hook to change the array, it won't update on render, so you will only see an empty array on debug.
As far as I can tell you do not return anything from the getPets() function.
Make use of the useState Function to save your doggies entries:
let [doggies, setDoggies ] = useState([]);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
return response.data;
} catch (err) {
// Handle Error Here
console.error(err);
}
return []
};
useEffect(() => {
setDoggies(await getPets());
});
I used setState inside the getPets function. Now it works.
const Collages = () => {
const [dogs, setData] = useState([]);
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);
I'm trying to set a state, which I fetch from an API in the form of an array.
Tried this in every way possible, doesn't work.
Any ideas on how to fix this?
instance is an axios.create that creates the instance to a localhost django server which has CORS-ALLOW-CROSS-ORIGIN True
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
fetchText();
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Do it like this,
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
useEffect(() => {
fetchText();
},[ any variable you want it to fetch that again ]);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
This looks to be a good use case for a useEffect hook. Also, you need to await async functions within useEffect statement. The useEffect hook cannot be an async function in itself. Also, the original implementation would result in an infinite loop. The setState function would trigger a re-render which would then trigger the fetch function to fire off again. Consider the following:
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const request= await instance.get(`/one/list/`);
request.then( r => setOne(r.data))
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Based on you guys' advice, I came up with the below code which works fine so far.
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const [el, setEl] = useState(null);
const fetchText = async () => {
let res = await instance.get("/one/list/");
setOne(res.data);
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
useEffect(() => {
const handleClick = e => {
let newEl = document.createElement("input");
newEl.value = e.target.innerHTML;
newEl.id = e.target.id;
newEl.addEventListener("keypress", e => handleInput(e));
newEl.addEventListener("focusout", e => handleInput(e));
e.target.parentNode.replaceChild(newEl, e.target);
newEl.focus();
};
const handleInput = async e => {
console.log(e.type);
if (
e.target.value !== "" &&
(e.key === "Enter" || e.type === "focusout")
) {
let payload = { text: e.target.value };
try {
e.preventDefault();
let res = await instance.put(`/one/update/${e.target.id}`, payload);
let text = res.data.text;
e.target.value = text;
let newEl = document.createElement("span");
newEl.innerHTML = text;
newEl.addEventListener("click", e => handleClick(e));
newEl.id = e.target.id;
e.target.parentNode.replaceChild(newEl, e.target);
} catch (e) {
console.error(e);
}
}
};
setEl(
one.map(o => (
<p>
<span id={o.id} onClick={e => handleClick(e)}>
{o.text}
</span>
</p>
))
);
}, [one]);
return <>{el}</>;
};
export default OneList;
I have this code
import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<Link to="/">Netflix</Link>
<Route path="/" component={Miliko} />
</div>
</Router>
);
}
const Miliko = ({ match }) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const Res = await fetch("https://foo0022.firebaseio.com/New.json");
const ResObj = await Res.json();
const ResArr = await Object.values(ResObj).flat();
setData(ResArr);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
console.log(data);
}, [match]);
return <div>{`${isLoading}${isError}`}</div>;
};
function App() {
return (
<div className="App">
<ParamsExample />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I created three links that open the Miliko component. but when I quickly click on the links I get this error:
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.
This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* #class useAsync
* #borrows useAsyncObject
* #param {object} _ props
* #param {async} _.asyncFunc Promise like async function
* #param {bool} _.immediate=false Invoke the function immediately
* #param {object} _.funcParams Function initial parameters
* #param {object} _.initialData Initial data
* #returns {useAsyncObject} Async object
* #example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
Without #windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.
In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.
const fetchSpecificItem = useCallback(async ({ itemId }) => {
try {
... fetch data
/*
Before you setState ensure the component is mounted
otherwise, return null and don't allow to unmounted component.
*/
if (!mountedRef.current) return null;
/*
if the component is mounted feel free to setState
*/
} catch (error) {
... handle errors
}
}, [mountedRef]) // add variable as dependency
I used useEffect to fetch data.
I could not call the function inside effect simply because hooks can not be called inside a function.
useEffect(() => {
fetchSpecificItem(input);
return () => {
mountedRef.current = false; // clean up function
};
}, [input, fetchSpecificItem]); // add function as dependency
Thanks, everyone your contribution helped me to learn more about the usage of hooks.
fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :
const Miliko = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
return function() {
/**
* Add cleanup code here
*/
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.
Folowing #Niyongabo solution, the way I ended up that fixed it was:
const mountedRef = useRef(true);
const fetchSpecificItem = useCallback(async () => {
try {
const ref = await db
.collection('redeems')
.where('rewardItem.id', '==', reward.id)
.get();
const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
if (!mountedRef.current) return null;
setRedeems(data);
setIsFetching(false);
} catch (error) {
console.log(error);
}
}, [mountedRef]);
useEffect(() => {
fetchSpecificItem();
return () => {
mountedRef.current = false;
};
}, [fetchSpecificItem]);
Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.
const mountedRef = useRef(true)
useEffect(() => {
// CALL YOUR API OR ASYNC FUNCTION HERE
return () => { mountedRef.current = false }
}, [])
const [getAllJobs, setgetAlljobs] = useState();
useEffect(() => {
let mounted = true;
axios.get('apiUrl')
.then(function (response) {
const jobData = response.data;
if (mounted) {
setgetAlljobs(jobData)
}
})
.catch(function (error) {
console.log(error.message)
})
return () => mounted = false;
}, [])
set a variable mounted to true->
then if it is true, mount the function->
in the bottom you return it to unmount it
My case was pretty different from what this questions wants. Still I got the same error.
My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)
If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..
React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.
The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.
I fixed this by adding different key prop value (not index), but some unique string.
you can wrap any action as a callback inside checkUnmount
const useUnmounted = () => {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);
const checkUnmount = useCallback(
(cb = () => {}) => {
try {
if (!mountedRef.current) throw new Error('Component is unmounted');
cb();
} catch (error) {
console.log({ error });
}
},
[mountedRef.current],
);
return [checkUnmount, mountedRef.current];
};
import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
onViewChange?: (n: number) => void;
userInit?: (user: IUser) => void;
}
interface State {
email: string;
password: string;
hasError?: boolean;
errorMessage?: string;
}
const initialValue = {
email: "eve.holt#reqres.in",
password: "cityslicka",
errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
const { onViewChange } = props;
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
const dispatch = useDispatch();
const handleEmailChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
email: val,
}));
},
[state.email]
);
const handlePasswordChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
password: val,
}));
},
[state.password]
);
const onUserClick = useCallback( async () => {
// HTTP Call
const data = {email: state.email , password: state.password}
try{
await dispatch(userLoginSuccessAction(data));
<Navigate to = '/' />
setState( (state)=>({
...state,
email: "",
password: ""
}))
}
catch(err){
setState( (state)=>({
...state,
errorMessage: err as string
}))
}
},[mountedRef] )
useEffect(()=>{
onUserClick();
return ()=> {
mountedRef.current = false;
};
},[onUserClick]);
const Error = (): JSX.Element => {
return (
<div
className="alert alert-danger"
role="alert"
style={{ width: "516px", margin: "20px auto 0 auto" }}
>
{state.errorMessage}
</div>
);
};
return (
<div>
<div>
email: "eve.holt#reqres.in"
<span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
</div>
{state.errorMessage && <Error />}
<form className="form-inline">
<div className="form-group">
<XTextField
label="email"
placeholder="E-Posta"
value={state.email}
onChange={handleEmailChange}
/>
</div>
<div className="form-group my-sm-3">
<XTextField
type="password"
label="password"
placeholder="Şifre"
value={state.password}
onChange={handlePasswordChange}
/>
</div>
<button type="button" className="btn btn-primary" onClick = {onUserClick} >
Giriş Et
</button>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(3);
}}
>
Şifremi Unuttum!
</a>
</form>
<p>
Hələdə üye deyilsiniz? <br />
pulsuz registir olmak üçün
<b>
<u>
<a
style={{ fontSize: "18px" }}
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(2);
}}
>
kilik edin.
</a>
</u>
</b>
</p>
</div>
);
};
export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For this problem I used a tricky way
first I deploy a state like this
const [routing,setRouting] = useState(false)
then when my works finished I changed it to true
and change my useEffect like this
useEffect(()=>{
if(routing)
navigation.navigate('AnotherPage')
),[routing]}