I have a pretty simple component in React that gets an image name and then displays it using the code below. It works fine but when I leave the page and then come back to it, I get a memory leak. Perhaps there is something basic about effects I am not understanding:
import React, {useState, useEffect} from 'react';
import {Storage} from 'aws-amplify';
const S3Image = ({photoName, ...props}) => {
const [imageURL, setImageURL] = useState("")
useEffect(() => {
var response = ""
async function s3Fetch() {
if (photoName !== "") {
response = Storage.get(photoName);
const data = await response;
setImageURL(data);
}
}
s3Fetch();
}, [imageURL])
return (
<>
{ imageURL === "" ?
null
:
<img {...props} src={imageURL} alt={photoName} />
}
</>
)};
export default S3Image
If I return a function at the end to clean up like:
return () => usetImageUrl("")
Then it goes in a crazy loop of rendering and then re-rendering.
Thanks for any help!
You are updating the imageURL in the effect and also rerun the effect if the url changes => Loop.
You need set the dependency array to [photoName] since that's the variable you are listening for and is the deciding factor in the fetch function:
import React, {useState, useEffect} from 'react';
import {Storage} from 'aws-amplify';
const S3Image = ({photoName, ...props}) => {
const [imageURL, setImageURL] = useState("")
useEffect(() => {
var response = ""
async function s3Fetch() {
if (photoName !== "") {
response = Storage.get(photoName);
const data = await response;
setImageURL(data);
}
}
s3Fetch();
}, [photoName])
return (
<>
{ imageURL === "" ?
null
:
<img {...props} src={imageURL} alt={photoName} />
}
</>
)};
export default S3Image
If you have a linter installed, it should tell you that as well.
#Domino987 answer was really helpful, but I needed to add the logic with "componentIsMounted" variable to get rid of the warning. We set the variable as false in the cleanup function of the useEffect. This way, we only set the imageUrl if the component is mounted.
import React, { useState, useEffect } from 'react';
import { Storage } from 'aws-amplify';
const S3Image = ({ imgKey, ...props }) => {
const [imageURL, setImageURL] = useState('');
useEffect(() => {
async function s3Fetch() {
if (imgKey !== '') {
try {
return Storage.get(imgKey, { level });
} catch (e) {
console.error(e);
}
}
}
let componentIsMounted = true;
s3Fetch().then((url) => {
if (componentIsMounted) {
if (url) setImageURL(url);
}
});
return () => {
componentIsMounted = false;
};
}, [imgKey]);
return (
<>
{imageURL === '' ? null : (
<img {...props} src={{ uri: imageURL }} />
)}
</>
);
};
export default S3Image;
Related
I am displaying a foto in the front using Leigh Halliday's Image Previews in React with FileReader from https://www.youtube.com/watch?v=BPUgM1Ig4Po and everything is super BUT:
1.I want to get information from the image is displaying, exactly the base64 info, and have it then globally in my reactjs app.
2.for that reason I made a Context, i configured it ok BUT:
when I am doing dispatch inside a useEffect I want the image rendering and the info store in my variable globally
but I have one thing or another
if my image renders ok in my front, I can not obtain the value of my dispatch and viceversa
this is the code of my component:
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AuthContext } from '../../auth/AuthContext'
import { types } from '../../types/types'
export const ButtonLoadFoto = () => {
const { dispatchFoto } = useContext(AuthContext)
const [image, setImage] = useState('')
const [preview, setPreview] = useState('')
const [status, setStatus] = useState(false)
useEffect(() => {
if (image) {
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result)
}
reader.readAsDataURL(image)
setStatus(true)
} else {
setPreview('')
}
}, [image])
// useEffect(() => {
// if (status) {
// dispatchFoto({
// type: types.foto,
// payload: {
// foto: preview.split(',')[1]
// }
// })
// }
// return () => setStatus(false)
// }, [preview])
const fileInputRef = useRef()
const handleRef = (e) => {
e.preventDefault()
fileInputRef.current.click()
}
const handleFile = (e) => {
const file = e.target.files[0]
if (file && file.type.substr(0, 5) === 'image') {
setImage(file)
}
}
return (
<div className='load-input '>
{
preview
?
(<img src={preview} alt='' onClick={() => setImage('')} />)
:
(<button
className='alert alert-danger'
onClick={handleRef}>
foto
</button>
)
}
< input
type='file'
style={{ display: 'none' }}
ref={fileInputRef}
accept='image/*'
onChange={handleFile}
/>
</div>
)
}
in the code above if you put away the comments we will have the information we want but the foto won t display at all
thanks all for your time , I really appreciate!
EDIT
this is the main component
import React, { useEffect, useReducer } from 'react'
import { AuthContext } from './auth/AuthContext'
import { fotoReducer } from './components/formScreen/fotoReducer'
import { AppRouter } from './routers/AppRouter'
const initImage = () => {
return { foto: '' }
}
export const CMI = () => {
const [foto, dispatchFoto] = useReducer(fotoReducer, {}, initImage)
return (
<div>
<AuthContext.Provider value={{
foto,
dispatchFoto
}}>
<AppRouter />
</AuthContext.Provider>
</div>
)
}
this is the componenent I use
import React, { useContext} from 'react'
import { ButtonLoadFoto } from '../components/formScreen/ButtonLoadFoto'
import { AuthContext } from '../auth/AuthContext'
export const FormScreen = () => {
const { foto } = useContext(AuthContext)
}
return (
<div>
<ButtonLoadFoto/>
</div>
)
as I said : if a render the image I can not have the information and viceversa...
when I use dispatch I don t know I it brokes the image render
thanks in advance
Hey guys I'm trying to show the data I get from firestore.
When I'm saving the code in the IDE and I'm on the current page, it is working.
But if then I go to another page/refresh the browser - it doesn't render/render in time and render the "hold" I set him to return
the code:
import React, { useState, useEffect } from 'react'
import firebase from 'firebase';
import { useAuth } from '../contexts/AuthContext';
export default function Cart() {
const [userMail, setUserMail] = useState(undefined)
const [userCart, setUserCart] = useState(undefined)
const user = useAuth()
const userDoc = firebase.firestore().collection("cart").doc(userMail)
useEffect(() => {
if (user.currentUser) {
setUserMail(user.currentUser.email, console.log(userMail))
userDoc.get().then((doc) => {
if (doc.exists) {
let cart = doc.data()
setUserCart(cart)
}
})
}
}, [])
if (userCart === undefined) return <h1>hold</h1>
const { item } = userCart
console.log(item);
return (
<main className="main-cart">
//here im try to make sure it got the data befor render//
{item && item.map(item => {
return (
<div key={item.itemId}>
<h3>{item.name}</h3>
</div>
)
})}
</main>
)
}
i just had to replace the 2nd parameter from the useEffect to userCart
I thought had a better grasp of hooks but I've clearly got something wrong here. Not all of the character objects will have what I'm trying to get but it wont work with those that do.
I cna't even build in a check for character.comics.available. Same errors appear. I'm presuming I'm getting them before the state is set? But {character.name} always works.
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
// from the Route path="/character/:id"
const { id } = useParams();
// custom hook. useCharacter.js
const [character] = useCharacter(id);
// this only works sometimes but errors if i refresh the page
// console.log(character.comics.available);
return (
<div>
<h2 className="ui header">Character Details</h2>
<p>Works every time: {character.name}</p>
<div className="ui segment"></div>
<pre></pre>
</div>
);
};
export default CharacterDetail;
Custom hook useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
const useCharacter = (id) => {
const [character, setCharacter] = useState({});
useEffect(() => {
loadItem();
return () => {};
}, [id]);
const loadItem = async (term) => {
const response = await marvel.get(`/characters/${id}`);
console.log(response.data.data.results[0]);
setCharacter(response.data.data.results[0]);
};
return [character];
};
export default useCharacter;
error when console is uncommented
Uncaught TypeError: Cannot read property 'available' of undefined
at CharacterDetail (CharacterDetail.js:11)
...
Here is the character object.
thanks to #Nikita for the pointers. Settled on this...
CharacterDetail.js
import React from "react";
import { useParams } from "react-router-dom";
import useCharacter from "../hooks/useCharacter";
const CharacterDetail = () => {
const { id } = useParams();
// custom hook. useCharacter.js
const { isLoading, character } = useCharacter(id);
const isArray = character instanceof Array;
if (!isLoading && isArray === false) {
console.log("isLoading", isArray);
const thumb =
character.thumbnail.path +
"/portrait_uncanny." +
character.thumbnail.extension;
return (
<div>
<h2 className="ui header">{character.name}</h2>
<img src={thumb} />
<div className="ui segment">{character.comics.available}</div>
<div className="ui segment">{character.series.available}</div>
<div className="ui segment">{character.stories.available}</div>
</div>
);
}
return <div>Loading...</div>;
};
export default CharacterDetail;
useCharacter.js
import { useState, useEffect } from "react";
import marvel from "../apis/marvel";
function useCharacter(id) {
const [character, setCharacter] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const fetchData = async () => {
setIsLoading(true);
await marvel
.get(`/characters/${id}`)
.then((response) => {
/* DO STUFF WHEN THE CALLS SUCCEEDS */
setIsLoading(false);
setCharacter(response.data.data.results[0]);
})
.catch((e) => {
/* HANDLE THE ERROR (e) */
});
};
fetchData();
}, [id]);
return {
isLoading,
character,
};
}
export default useCharacter;
I have trouble with react context.
Especially with function getProductCategory which I use in the second component.
My React context provider looks like :
import React, { createContext, useState, useEffect } from "react";
import data from "./data";
export const ProductContext = createContext();
const ProductContextProvider = ({ children }) => {
const [productsCategory, setProductCategory] = useState();
const [products, setProducts] = useState();
useEffect(() => {
setProducts(data);
});
function getProductCategory(category) {
const productFromCategory = data.filter(
(product) => product.type === category
);
setProductCategory(productFromCategory); //this line is causing the problem
console.log(productFromCategory);
return productsCategory;
}
const getProduct = (product) => {
const currentProduct = products
? products.filter((item) => item.slug === product)[0]
: undefined;
return currentProduct;
};
return (
<ProductContext.Provider
value={{ getProduct, getProductCategory }}
>
{children}
</ProductContext.Provider>
);
};
export default ProductContextProvider;
I want to get access to my context from another component Mats which is a page.
import React, { useContext, useEffect } from "react";
import { ProductContext } from "../../context";
/accesoriesComponent/CategoryTitle/CategoryTitle";
import Filters from "../../components/accesoriesComponent/Filters/Filters";
import ProductWrapper from "../../components/accesoriesComponent/ProductWrapper/ProductWrapper";
export default function Mats(props) {
const { getProductCategory } = useContext(ProductContext);
const mats = getProductCategory("mats");
return (
<div>
<div style={{ display: "flex" }}>
<Filters />
{mats && <ProductWrapper products={mats} />}
</div>
</div>
);
}
When I want to visit page Mats, I get error "Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside component WillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.".
I marked the line of code in the first code, that courses the problem .
I don't know how to handle it.
Any suggestions ?
Based on your code:-
instead of return-ing productsCategory from your getProductCategory function. Send your productsCategory as ProductContext.Provider values props
Btw, your useEffect should have [] or empty array as dependency
ProductContextProvider.js:-
import React, { createContext, useState, useEffect } from "react";
import data from "./data";
export const ProductContext = createContext();
const ProductContextProvider = ({ children }) => {
const [productsCategory, setProductCategory] = useState();
const [products, setProducts] = useState();
useEffect(() => {
setProducts(data);
}, []); // making sure it will only run once when rendered
function getProductCategory(category) {
const productFromCategory = data.filter(
(product) => product.type === category
);
setProductCategory(productFromCategory); // you already set the data in here
console.log(productFromCategory);
// return productsCategory; // no need to return this
}
const getProduct = (product) => {
const currentProduct = products
? products.filter((item) => item.slug === product)[0]
: undefined;
return currentProduct;
};
return (
<ProductContext.Provider
value={{ productsCategory, getProduct, getProductCategory }}
>
{children}
</ProductContext.Provider>
);
};
export default ProductContextProvider;
So in Mats.js, just get display your updated ``
export default function Mats(props) {
const { productsCategory, getProductCategory } = useContext(ProductContext);
// Shouldn't do this. Cause it will keep rerender
// const mats = getProductCategory("mats");
// default state of category
const [category, setCategory] = useState('mats')
// use useffect to send the updated products by category
useEffect(() => {
(() => {
if(category) {
getProductCategory(category);
}
})()
}, [category]) // will initiated every time category change
return (
<div>
<div style={{ display: "flex" }}>
<Filters />
{productsCategory && <ProductWrapper products={productsCategory} />}
</div>
</div>
);
}
I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.
https://codesandbox.io/s/8x5lzjmwl8
Please review my code and find my flaw.
useAxiosFetch.js
import {useState, useEffect} from 'react'
import axios from 'axios'
const useAxiosFetch = url => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
let source = axios.CancelToken.source()
useEffect(() => {
try {
setLoading(true)
const promise = axios
.get(url, {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log(`request cancelled:${thrown.message}`)
} else {
console.log('another error happened')
}
})
.then(a => {
setData(a)
setLoading(false)
})
} catch (e) {
setData(null)
setError(e)
}
if (source) {
console.log('source defined')
} else {
console.log('source NOT defined')
}
return function () {
console.log('cleanup of useAxiosFetch called')
if (source) {
console.log('source in cleanup exists')
} else {
source.log('source in cleanup DOES NOT exist')
}
source.cancel('Cancelling in cleanup')
}
}, [])
return {data, loading, error}
}
export default useAxiosFetch
index.js
import React from 'react';
import useAxiosFetch from './useAxiosFetch1';
const index = () => {
const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
const {data,loading} = useAxiosFetch(url);
if (loading) {
return (
<div>Loading...<br/>
<button onClick={() => {
window.location = "/awayfrom here";
}} >switch away</button>
</div>
);
} else {
return <div>{JSON.stringify(data)}xx</div>
}
};
export default index;
Here is the final code with everything working in case someone else comes back.
import {useState, useEffect} from "react";
import axios, {AxiosResponse} from "axios";
const useAxiosFetch = (url: string, timeout?: number) => {
const [data, setData] = useState<AxiosResponse | null>(null);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let unmounted = false;
let source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token,
timeout: timeout
})
.then(a => {
if (!unmounted) {
// #ts-ignore
setData(a.data);
setLoading(false);
}
}).catch(function (e) {
if (!unmounted) {
setError(true);
setErrorMessage(e.message);
setLoading(false);
if (axios.isCancel(e)) {
console.log(`request cancelled:${e.message}`);
} else {
console.log("another error happened:" + e.message);
}
}
});
return function () {
unmounted = true;
source.cancel("Cancelling in cleanup");
};
}, [url, timeout]);
return {data, loading, error, errorMessage};
};
export default useAxiosFetch;
Based on Axios documentation cancelToken is deprecated and starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
//...
React.useEffect(() => {
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
return () => {
controller.abort();
};
}, []);
//...
The issue in your case is that on a fast network the requests results in a response quickly and it doesn't allow you to click the button. On a throttled network which you can achieve via ChromeDevTools, you can visualise this behaviour correctly
Secondly, when you try to navigate away using window.location.href = 'away link' react doesn't have a change to trigger/execute the component cleanup and hence the cleanup function of useEffect won't be triggered.
Making use of Router works
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
import useAxiosFetch from './useAxiosFetch'
function App(props) {
const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
const {data, loading} = useAxiosFetch(url)
// setTimeout(() => {
// window.location.href = 'https://www.google.com/';
// }, 1000)
if (loading) {
return (
<div>
Loading...
<br />
<button
onClick={() => {
props.history.push('/home')
}}
>
switch away
</button>
</div>
)
} else {
return <div>{JSON.stringify(data)}</div>
}
}
ReactDOM.render(
<Router>
<Switch>
<Route path="/home" render={() => <div>Hello</div>} />
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('root'),
)
You can check the demo working correctly on a slow network
Fully cancellable routines example, where you don't need any CancelToken at all (Play with it here):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}
This is how I do it, I think it is much simpler than the other answers here:
import React, { Component } from "react";
import axios from "axios";
export class Example extends Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
axios.get("/data").then((res) => {
if (this._isMounted && res.status === 200) {
// Do what you need to do
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div></div>;
}
}
export default Example;