how I test useEffect with isLoading state - reactjs

I want to build test when the isLoading state change the component.
I know in the class component there is the way by do setState with enzyme, but I would like to know how I can do it here.
const Spacex = () => {
const [open, setOpen] = useState(false);
const [upComingLaunches, setUpComingLaunches] = useState([]);
const [Launchpad, setLaunchpad] = useState([])
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let tempData;
SpaceXNextLaunche()
.then(data => {
setUpComingLaunches(data);
tempData = data;
return LaunchPad()
}).then(dataLaunch => {
const foundTheLaunch = dataLaunch.docs.filter((Launch, index) => {
return tempData.id === Launch.id
});
setLaunchpad(foundTheLaunch);
setIsLoading(false);
})
}, [])
if (isLoading) return <LoadingComp />
return (
<div>
<div className="upcoming-launches">
<h1 className={styles.title}>upcoming launche</h1>
<div className={styles.CountDownWarrper}>
{Object.keys(upComingLaunches).length > 0 ?
<Card className={styles.CountDownCard}>
<div className={styles.MissionName}>{upComingLaunches.name}</div>
<div className={styles.gridBadges}>
<div className={styles.CountDown}><CountDownClock upComingLaunches={upComingLaunches} /></div>
<div className={styles.badgeFlex}><img className={styles.badge} src={upComingLaunches.links["patch"]["small"]} alt="mission patch" /></div>
</div>
<GoogleMap
mapVisiblity={(e) => setOpen(!open)}
open={open}
placeName={Launchpad[0].launchpad.full_name} />
</Card>
: null}
</div>
</div>
</div>
)
}
export default Spacex;

The proper way to test functional components is to test the actual functions' behaviour, not their implementation. In your case that would be mocking the SpaceXLaunche() to return its data after some timeout, eg:
function SpaceXLauncheMock() {
return new Promise(resolve => {
setTimeout(resolve(data), 1500);
});
}
const SpaceXLaunche = jest.spyOn(SpaceXLaunche.prototype, 'SpaceXLaunche')
.mockImplementation(SpaceXLauncheMock);
then, you'd test your consequence of isLoading - the presence or absence of LoadingComp, initially, and again after the timeout (don't forget to put done as the test case's argument):
expect(component.contains(<LoadingComp />)).toBe(true);
setTimeout(() => {
expect(component.contains(<LoadingComp />)).toBe(false);
done();
}, 2000);

Related

Reactjs Sort By Price

I want to sort my product after I click the text "Sort by Log To High",
I put "product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)" in a onClick function but it does not work. Now it works only if I put in the const displayProduct.
Any tutorial or video may I refer to? Thanks for helping.
export const Product = () =>{
const [product, setProduct] = useState([]);
const [pageNumber, setPageNumber] = useState(0)
const productPerPage = 12
const pagesVisited = pageNumber * productPerPage
const displayProduct = product
.slice(pagesVisited,pagesVisited + productPerPage)
.map(product => {
return(
<div className='imageContainer ' key={product.id}>
<img src={PopularOne} className="image"/>
<div className='productName'>
<Link style={{ textDecoration:'none' }} to="/productsDetails" state={{ product:product }}>{product.productName}</Link>
</div>
<div className='productPrice'>
<h3 >RM{product.productPrice}</h3>
</div>
</div>
)
})
//product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)
const pageCount = Math.ceil(product.length/ productPerPage)
const changePage = ({selected}) =>{
setPageNumber(selected)
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(`/routes/getProduct`);
console.log(res)
setProduct(res.data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
return(
<div className='product'>
<div>
<button><h3>Sort By Low to High</h3></button>
<h3>Sort By High to Low</h3>
</div>
<div className='productContainer'>
{displayProduct}
</div>
<ReactPaginate
previousLabel={""}
nextLabel={""}
breakLabel="..."
pageRangeDisplayed={5}
pageCount={pageCount}
onPageChange={changePage}
containerClassName={"pagination"}
breakClassName={"break"}
pageClassName={"page-item"} //li
pageLinkClassName={"page-link"} //a
activeLinkClassName={"page-link-active"}
/>
<Footer/>
</div>
)
}
When you use the useState function provided by React it returns 2 things, first is the state variable, and second is the updater function. In your case the state is product and the updater is setProduct.
It doesn't work because you are trying to modify the state variable, just use the updater function, and it will work.
For example:
setProduct(prevState => {
let newState = [...prevState];
newState.sort((a, b) => a.productPrice > b.productPrice ? 1 : -1);
return newState;
});
Updater function provides the previous state, in this case it's named prevState.
Shallow clone the array and store it in the newState
variable.
Mutate the newState array via the sort method.
Return the newState. By returning here we tell React to update the state to the value of newState.

Why I'm getting Rendered more hooks error?

I was wonder what I'm doing wrong here.
I'm getting this error: "Rendered more hooks than during the previous render."
export default function ProductDetails() {
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
//Extract Product Data
const {title,description, image, gallery } = data.products.data[0].attributes;
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
console.log(img);
//Create a toast
const notify = () => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify();
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
}
Something wrong is in this line: const [img, setImg] = useState();
Why I can't use more hooks here.
Does anyone know why I'm getting this?
You are using early return
and this line of code won't execute every time:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
This is only conditionally called:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
Because the component has earlier conditional return statements. Move it to earlier in the function. (Generally I invoke useState operations right away.)
Hooks need to always be consistently called in the same order on every render.
You declare your state after some return statements. It means that if you had any errors or you were in loading state, the state is not defined. But maybe in the next render, the data is set and then your state will be defined with the inital value (gallery.data[0].attributes.formats.medium.url).
It's forbidden in react because all of the hooks should always be in the same order on every single render. In order to fix this, you should change the place of your useState for img.
Hope it helps:
export default function ProductDetails() {
const [img, setImg] = useState('');
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
//Extract Product Data
useEffect(() => {
if(results && results.data) {
const {data} = results
const { gallery } = data.products.data[0].attributes;
setImg(gallery.data[0].attributes.formats.medium.url);
}
}, [results]);
useEffect(() => {
console.log(img);
}, [img]);
//Create a toast
const notify = (title) => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
if(fetching) {
return <p>Loading...</p>;
} else if(error) {
return <p>Oh no....</p>;
} else if(data) {
const { title, description, image, gallery } = data.products.data[0].attributes;
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify(title);
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
} else {
return null;
}
}
There should be no return before hooks.
These lines
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
should be after all hooks

How to cleanup useRef in useEffect?

I have this component, so I want to clean up in useEffect. I googled the issue and there is no helpful information.
const LoadableImg = ({src, alt}) => {
const [isLoaded, setIsLoaded] = useState(false);
let imageRef = useRef(null);
useEffect(() => {
if (isLoaded) return;
if (imageRef.current) {
imageRef.current.onload = () => setIsLoaded(true);
}
return () => {
imageRef.current = null;
};
}, [isLoaded]);
return (
<div className={isLoaded ? 'l_container_loaded' : 'l_container'}>
<img ref={imageRef} className={isLoaded ? "l_image_loaded" : 'l_image'}
src={src}
alt={alt}/>
</div>
) };
I can't figure out how to clean up in useEffect.
UPDATE
added another useEffect, according to Arcanus answer.
const LoadableImg = ({src, alt}) => {
const [isLoaded, setIsLoaded] = useState(false);
let imageRef = useRef(null);
useEffect(() => {
if (isLoaded) return;
if (imageRef.current) {
imageRef.current.onload = () => setIsLoaded(true);
}
}, [isLoaded]);
useEffect(() => {
return () => {
imageRef.current = null;
};
},[])
return (
<div className={isLoaded ? 'l_container_loaded' : 'l_container'}>
<img ref={imageRef} className={isLoaded ? "l_image_loaded" : 'l_image'}
src={src}
alt={alt}/>
</div>
)};
If you want to do this with a ref, then you will need to remove the onload function, but you do not need to null out imageRef.current:
useEffect(() => {
if (isLoaded) return;
const element = imageRef.current;
if (element) {
element.onload = () => setIsLoaded(true);
return () => {
element.onload = null;
}
}
}, [isLoaded]);
That said, i recommend you do not use a ref for this. A standard onLoad prop will work just as well, without the need for all the extra logic for adding and removing the event listener:
const LoadableImg = ({ src, alt }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div className={isLoaded ? "l_container_loaded" : "l_container"}>
<img
className={isLoaded ? "l_image_loaded" : "l_image"}
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
/>
</div>
);
};
In your instance, the only time you want to use useEffect is when DOM is fully loaded, and your ref is ready. Hence you need a hook
E.g.
function useHasMounted() {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
Then your Component should be corrected to be as follows
const hasMounted = useHasMounted();
useEffect(() => {
if (hasMounted) {
imageRef.current.onload = () => setIsLoaded(true);
}
}, [hasMounted]); //<-- call once when dom is loaded.
I understand you want to call onload whenever images is loaded, however, please do note this do not always work because images loaded from cache does not call onload.

How to access nested Obj in react?

Why some attributes inside this weather Obj are accessable(first layer), but second layer cannot be accessed? See below picture for Obj.
with these code:
const Weather=(props)=>{
const{capital, api_key} = props
const [weather, setWeather] = useState('')
useEffect(() => {
axios
.get(`http://api.openweathermap.org/data/2.5/weather?q=${capital}&units=metric&appid=${api_key}`)
.then(response => {
setWeather(response.data)
})
}, [])
console.log('weather', weather)
return(
<div>
<div> temperature {weather.visibility} Celcius</div>
<img src='http://openweathermap.org/img/wn/03n#2x.png' alt="flag" width="120" height="100"></img>
<p>wind {weather.main.temp} m/s</p>
</div>
)
}
I can access e.g.,{weather.visibility}, but not {weather.main.temp}. Do you know why is it the case?
There is something wrong with your url, also on the first render weather.main.temp is undefined, because the api request is async code. The code below is working here:
const Weather=(props)=>{
const{capital, api_key} = props;
const [weather, setWeather] = useState('');
useEffect(() => {
axios
.get(`https://api.openweathermap.org/data/2.5/weather?q=${capital}&appid=${api_key}`)
.then(response => {
setWeather(response.data)
})
}, [])
console.log('weather', weather)
return(
<div>
<div> temperature {weather.visibility} Celcius</div>
<img src='http://openweathermap.org/img/wn/03n#2x.png' alt="flag" width="120" height="100"></img>
<p>wind {weather?.main?.temp} m/s</p>
</div>
)
}

Prefilled slider value, jumps always back to default state of prefilled api data

Hello i have a prefilled slider which is for rating, i prefill it with data from the API and if a user slides i like to update the API Database. Everything works fine except the slider always jumps back to the prefilled value by useEffect. How can i change the state so that the value from the user takes precedence over the prefilled value ?
This is my code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/xxxxxxxx/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
setValue(arrayItem.rating);
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const changeSlider = (e) => {
// This value should override the default value from the api
setValue(e.currentTarget.value);
};
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => changeSlider(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Thanks help is appreciated.
I finally managed to solve it. I needed to give one of the two setValue precedence over the other based on the condition that the user slided the slider. Up to now i didnt know useRef, with this hook i could manage it. Here is the code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
const ref = useRef(0);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/jensgeffken/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
//console.log(ref.current)
if(ref.current === 0) {
setValue(arrayItem.rating);
}
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const handleSlide = (e) => {
ref.current = e.currentTarget.value
setValue(ref.current);
}
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => handleSlide(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Following your general approach, this seems to work fine for me:
const fetch = () => new Promise(resolve => setTimeout(() =>
resolve({json: () => ({rating: 4})})
, 1000));
const App = () => {
const [sliderVal, setSliderVal] = React.useState(0);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch("foo")
.then(res => res.json())
.then(data => {
setLoading(false);
setSliderVal(data.rating);
});
}, []);
const handleSliderChange = evt => {
setSliderVal(evt.target.value);
};
return (
<div>
{loading
? <p>loading...</p>
: <div>
<input
type="range"
min="1"
max="10"
value={sliderVal}
onChange={handleSliderChange}
/>
{sliderVal}
</div>
}
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>

Resources