React - UseState not triggering rerender following fetch inside useEffect hook - reactjs

I am just working through my first React tutorial using hooks - I am using trying to fetch local data within useEffect and then update the state using useState. I am then passing the state into a Context.provider which a child element (Card.js) subscribes to. Even though useEffect runs, the Card component and the state isn't being rerendered/updated. What am I doing wrong?
MainContext.js -
import React, { createContext, useState, useContext, useEffect, Fragment } from 'react';
import List from '../containers/List';
export const myContext= createContext();
export const MainContext = ({children}) => {
const [films, setFilms] = useState([]);
const loadData = () => {
try {
fetch('../src/assets/data.json').then( result => result.json()).then(movies => {
setFilms(films)
})
} catch (error) {
console.log('there has been an error')
}}
useEffect(() => {
loadData()
},[]);
return (
<myContext.Provider value={{films,setFilms }}>
{children()}
</myContext.Provider>
);
}
Card.js -
function Card() {
const {films} = useContext(myContext)
if (films !== undefined) {
return (
<div>
{ films.map((movie,i) => {
return (
<div key={i}>
<img src={movie.img.src} className='card-img-top' alt={movie.img.alt} />
<div className='card-body'>
<h2 className='card-title'>{`#${movie.ranking} - ${movie.title} (${movie.year})`}</h2>
</div>
<ul className='list-group list-group-flush'>
<li className='list-group-item'>{`Distributor: ${movie.distributor}`}</li>
<li className='list-group-item'>{`Amount: ${movie.amount}`}</li>
</ul></div>
)
})
}
</div>
)
} else {
return <div>{'Update Failed'}</div>
}
}
export default Card

You don't have any dependency in your useEffect array. This is why it doesn't trigger again once the app is mounted. You need to pass it a dependency, so it can run again each time the dependency value changes. Also, consider adding async before your loadData function.

Related

useQuery - can not read properties of undefined

Initial query returns undefined, but all subsequent calls return valid data. If I try to map my response, an error is thrown:
can not read properties of undefined
and the whole app is down. How do i fix this?
import { useQuery } from "#apollo/client";
import { Component } from "react";
import { useState, useEffect } from "react";
import GET_PRODUCTS from '../../server/getProducts';
import './productList.sass';
class ProductList extends Component {
render() {
return (
<RenderProducts />
);
}
}
const RenderProducts = () => {
const { data } = useQuery(GET_PRODUCTS);
console.log(data.products.map(product => console.log(product)));
const products = data.products.map((product) => {
return (
<li className="productList__item">
<img className="productList__item-img" src={product.mainImage.url} alt={product.title} />
<div className="productList__item-descr">
<div className="productList__item-title">{product.title}</div>
<div className="productList__item-price">{product.price} $</div>
</div>
</li>
)
})
return <ul>{products}</ul>
}
export default ProductList;
If I try to map my response, an error is thrown:
and the whole app is down. How do i fix this?
You'll need to render something when the query is in a loading state. You can take advantage of the loading and error properties of useQuery hook. Here's a sample:
const RenderProducts = () => {
const { data, loading, error } = useQuery(GET_PRODUCTS);
if(loading) return <div>loading...</div>
if(error) return <div>cannot render products...something went wrong</div>
// if the query has finished loading products and there's no error,
// You can access data.products
// and write your logic
console.log(data.products.map(product => console.log(product)));
const products = data.products.map((product) => {
return (
<li className="productList__item">
<img className="productList__item-img" src={product.mainImage.url} alt={product.title} />
<div className="productList__item-descr">
<div className="productList__item-title">{product.title}</div>
<div className="productList__item-price">{product.price} $</div>
</div>
</li>
)
})
return <ul>{products}</ul>
}

React hook for page scroll position causing re-renders on scroll

I'm using a React hook to track the scroll position on a page. The hook code is as follows:
import { useLayoutEffect, useState } from 'react';
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(window.pageYOffset);
useLayoutEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
export default useScrollPosition;
I then use this in various ways, for example in this component where a class is applied to an element if the page has scrolled more than 10px:
const Component = () => {
const scrollPosition = useScrollPosition();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const newScrolled = scrollPosition > 10;
if (newScrolled !== scrolled) {
setScrolled(newScrolled);
}
}, [scrollPosition]);
return (
<div
className={clsx(style.element, {
[style.elementScrolled]: scrolled,
})}
>
{children}
</div>
);
};
This all works and does what I'm trying to achieve, but the component re-renders continuously on every scroll of the page.
My understanding was that by using a hook to track the scroll position, and by using useState/useEffect to only update my variable "scrolled" in the event that the scroll position passes that 10px threshold, the component shouldn't be re-rendering continuously on scroll.
Is my assumption wrong? Is this behaviour expected? Or can I improve this somehow to prevent unnecessary re-rendering? Thanks
another idea is to have your hook react only if the scroll position is over 10pixel :
import { useEffect, useState, useRef } from 'react';
const useScrollPosition = () => {
const [ is10, setIs10] = useState(false)
useEffect(() => {
const updatePosition = () => {
if (window.pageYOffset > 10) {setIs10(true)}
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return is10;
};
export default useScrollPosition;
import React, { useEffect, useState } from "react";
import useScrollPosition from "./useScrollPosition";
const Test = ({children}) => {
const is10 = useScrollPosition();
useEffect(() => {
if (is10) {
console.log('10')
}
}, [is10]);
return (
<div
className=''
>
{children}
</div>
);
};
export default Test
so your component Test only renders when you reach that 10px threshold, you could even pass that threshold value as a parameter to your hook, just an idea...
Everytime there is useState, there will be a re-render. In your case you could try useRef to store the value instead of useState, as useRef will not trigger a new render
another idea if you want to stick to your early version is a compromise :have the children of your component memoized, say you pass a children named NestedTest :
import React from 'react'
const NestedTest = () => {
console.log('hit nested')
return (
<div>nested</div>
)
}
export default React.memo(NestedTest)
you will see that the 'hit nested' does not show in the console.
But that might not be what you are expecting in the first place. May be you should try utilizing useRef in your hook instead

React , Next js , state keeps it's default value after rerender

I have a filters states which are responsable for showing and hiding the specified filter. When i choose a value from the filter the page has to fetch data from api , populate the component with new data (component rerenders) , and the menu stay visible and the filter expanded. This code worked before migrating to next js , now when i rerender the component the state takes it's default value of (false) and the filter in not expanded. Here is some code
This is the main component. What im interestet in is FiltersMenu component.
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router';
import FiltersMenu from '../../../../../components/Filters/FiltersMenu';
export async function getServerSideProps(context) {
const productsType = context.params.products;
const res = await fetch(some api)
const data = await res.json();
return {
props: {
products:data.data,
price:{
minPrice:data.minPrice,
maxPrice:data.maxPrice
},
avaibleSizes:data.avaibleSizes
},
}
}
export default Products
function Products(props) {
const [isLoaded,setIsLoaded] = useState(false);
const [showFilters,setShowFilters] = useState(false);//show filters menu or not
const [products,setProducts] = useState();//data of the products fetcher from backend
const [filterValues,setFilterValues]=useState({price:0,color:[],size:[]});//values to send to the server to filter
const toogleShowFilters = (bool) =>{
setShowFilters(bool);
}
const handleSetFilterValues = (e) =>{
//changes values of filterValues
//and after that set isLoaded to false
}
const filterProducts = () =>{
fetch(some api)
.then(res=>res.json())
.then(data=>{
setProducts(data);
setIsLoaded(true);
})
}
useEffect(()=>{
filterProducts();
},[filterValues])
useEffect(()=>{
setProducts(props.products);
setIsLoaded(true);
},[])
if(isLoaded === false) return <p>Loading</p>
return (
<div>
<div className='products-heading'>
<FiltersMenu
showFilters={showFilters}
toogleShowFilters={toogleShowFilters}
price={props.price}
avaibleSizes={props.avaibleSizes}
filterValues={filterValues}
handleSetFilterValues={handleSetFilterValues}
/>
</div>
</div>
)
}
Here is how Filters menu component looks
import React,{useState , useRef, useEffect} from 'react'
import PriceFilter from './PriceFilter';
function FiltersMenu(props) {
const [showPriceFilter,setShowPriceFilter] = useState(false);
const filtersMenu = useRef();
const showFiltersMenu = () =>{
filtersMenu.current.className="filters animate__animated animate__slideInUp";
props.toogleShowFilters(true);
console.log(props.showFilters + " t ");
}
const hideFiltersMenu = () =>{
filtersMenu.current.className="filters animate__animated animate__slideOutDown";
setTimeout(()=>{filtersMenu.current.className='hidden'},1000);
props.toogleShowFilters(false);
console.log(props.showFilters + " t ");
}
useEffect(()=>{
if(props.showFilters===false){
hideFiltersMenu();
}
},[])
return (
<div>
<div onClick = {showFiltersMenu} className='filters-button'>
<img src='/images/filter.png'/>
Filters
</div>
{
<div ref={filtersMenu} className="filters">
<div className='heading'>
<h2>Filters</h2>
<img onClick = {hideFiltersMenu} src='/images/close.png' className='close-icon'/>
</div>
<div
onClick={()=>{setShowPriceFilter((prev)=>!prev)}}
className={"holder" +" " + (showPriceFilter===true && "highlight")}>
<div>Price</div>
<i className="fa-solid fa-caret-down"></i>
</div>
{
showPriceFilter && <PriceFilter price={props.price}
filterValues={props.filterValues}
handleSetFilterValues={props.handleSetFilterValues}
/>
}
</div>
}
</div>
)
}
export default FiltersMenu;
And PriceFilter component
import React, { useState } from 'react'
function PriceFilter(props) {
const [currcentPrice,setCurrentPrice] = useState(props.price.maxPrice);
const handleChange = (e) =>{
props.handleSetFilterValues(e);
}
const andjustPrice = (e) =>{
setCurrentPrice(e.target.value);
}
return (
<div className='price-filter animate__animated animate__flipInX'>
<p className='current-price'>{currcentPrice}$</p>
<div className='slider-holder'>
<p>{props.price.minPrice}</p>
<input onChange={andjustPrice} onMouseUp={handleChange} value={currcentPrice}
type="range" min={props.price.minPrice} max={props.price.maxPrice + 1} name='price' />
<p>{props.price.maxPrice + 1}</p>
</div>
</div>
)
}
export default PriceFilter
Every time i change the price the All components rerender , the main components keeps it's state values , but FiltersMenu takes the default values.
Found a solution , the problem is in
if(isLoaded === false) return <p>Loading</p>
When filterValues are changed isLoaded is set to false therefor the FiltersMenu component doesen't render , after that when isLoaded is true again , FiltersMenu is rendered again and thats the reason the state in FiltersMenu has default values , to fix it simply isLoaded can be used in return as that
return (
{
isLoaded === true && <MyComponent />
}
)

Filter useContext state in a component without changing global state

I have a component that uses useContext to retrieve the global state, which is a list of movies/TV shows. I'm trying to save this global state into local state so I can filter the list by media type (movie or TV show) on a button click, but the filter should be reflected only locally, without dispatching an action to change the global state (I don't want the complete list to be lost).
However, after storing the global state to the local state, the local state is empty. Here is the component's code:
import React, { useContext, useState, useEffect } from "react";
import { WatchListContext } from "../../contexts/WatchListProvider";
import WatchItem from "../WatchItem";
const WatchList = () => {
const { state } = useContext(WatchListContext);
const [watchList, setWatchList] = useState(state);
const [filter, setFilter] = useState("");
useEffect(() => {
setWatchList((previousWatchlist) => {
const filteredWatchList = previousWatchlist.filter(
(watchItem) => watchItem.media_type === filter
);
return filteredWatchList;
});
}, [filter]);
return (
<div>
<div>
<button onClick={() => setFilter("tv")}>TV</button>
<button onClick={() => setFilter("movie")}>MOVIES</button>
</div>
{watchList
.filter((watchItem) => watchItem.media_type === filter)
.map(({ id, title, overview, poster_url, release_date, genres }) => (
<WatchItem
key={id}
title={title}
overview={overview}
poster_url={poster_url}
release_date={release_date}
genres={genres}
/>
))}
</div>
);
};
export default WatchList;
I can't understand why this isn't working. Any help will be much appreciated!
You can't straightaway ties your global state with your local state like the way you did (const [watchList, setWatchList] = useState(state);):-
by doing this it will automatically ties with your default value of your state declared in context which always gonna be an empty array or [] . So to go around this, why don't you track the incoming changes of state with useEffect, like so:-
WatchList.js:-
import React, { useContext, useState, useEffect } from "react";
import { WatchListContext } from "../../contexts/WatchListProvider";
import WatchItem from "../WatchItem";
const WatchList = () => {
const { state } = useContext(WatchListContext);
const [watchList, setWatchList] = useState([]);
const [filter, setFilter] = useState("");
// handle fetching current changes of state received by Context
useEffect(() => {
(() => setWatchList(state))()
}, [state])
// handle filtering of data
useEffect(() => {
setWatchList((previousWatchlist) => {
const filteredWatchList = previousWatchlist.filter(
(watchItem) => watchItem.media_type === filter
);
return filteredWatchList;
});
}, [filter]);
return (
<div>
<div>
<button onClick={() => setFilter("tv")}>TV</button>
<button onClick={() => setFilter("movie")}>MOVIES</button>
</div>
{watchList
.filter((watchItem) => watchItem.media_type === filter)
.map(({ id, title, overview, poster_url, release_date, genres }) => (
<WatchItem
key={id}
title={title}
overview={overview}
poster_url={poster_url}
release_date={release_date}
genres={genres}
/>
))}
</div>
);
};
export default WatchList;

React: Access to the (updated) state with useState: it is not updated inside the component that creates it, but it is outside where it is called

Why am I not having access to the updated recipes (useState) value from inside the component that defines it?
In this example you can see how not being able to access to this value causes an error in the app once the reference to a function that I use to update the state is deleted
=> Codebox and code below
*Click two times the <h1> to see the error
https://codesandbox.io/s/sparkling-sea-5iqgo?fontsize=14&hidenavigation=1&theme=dark
import React, { useEffect, useState } from "react";
export default function App() {
const [userRecipes, setUserRecipes] = useRecipesData();
return (
<div className="App">
<h1
onClick={() => {
userRecipes.setBookmarks("onetwothree");
}}
>
Hello CodeSandbox
</h1>
<h2>{userRecipes.bookmarked_recipes}</h2>
</div>
);
}
const useRecipesData = () => {
const [recipes, setRecipes] = useState({});
const setBookmarks = newRecipes => {
console.log(recipes); // is undefined !? and deletes setBookmarks
setRecipes({
bookmarked_recipes: newRecipes,
setBookmarks: recipes.setBookmarks
});
};
useEffect(() => {
setRecipes({
bookmarked_recipes: "testtesttest",
setBookmarks: setBookmarks
});
}, []);
return [recipes, setRecipes];
};
What I don't understand is why if I return [recipes, setRecipes] where recipes.setBookmarks stores a reference to a function, it doesn't work
But if I return the function itself (which is a reference as well) [recipes, setBookmarks] then it works
See this other codebox where it does work
https://codesandbox.io/s/red-violet-gju99?fontsize=14&hidenavigation=1&theme=dark
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [userRecipes, setUserRecipes] = useRecipesData();
return (
<div className="App">
<h1
onClick={() => {
setUserRecipes("onetwothree" + Math.random());
}}
>
Hello CodeSandbox
</h1>
<h2>{userRecipes.bookmarked_recipes}</h2>
</div>
);
}
const useRecipesData = () => {
const [recipes, setRecipes] = useState({});
const setBookmarks = newRecipes => {
console.log(recipes); // is defined this time
setRecipes({
bookmarked_recipes: newRecipes,
setBookmarks: recipes.setBookmarks
});
};
useEffect(() => {
setRecipes({
bookmarked_recipes: "testtesttest",
setBookmarks: setBookmarks
});
}, []);
return [recipes, setBookmarks];
};
It's all about context.
If you'll put console.log(receipes) in useEffect and the render function itself, you can see what the flow of events are:
First render recipe is empty.
UseEffect is called and puts setBookmark in recipe (but the recipe for setBookmark is empty)
Second render is called, and now recipe has "testesttest" and recipe.setBookmark is a function where the recipe object that is bound to it is the recipe value from event 1
setBookmark is called, recipe is now set to "onetwothree" but the recipe object is empty so we set the setBookmark to undefined.
instead of keeping the function inside the state, you need to just call it directly (I.E. return setBookmark and not setRecipes, like this:
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [userRecipes, setBookmarks] = useRecipesData();
return (
<div className="App">
<h1
onClick={() => {
setBookmarks("onetwothree" + Math.random());
}}
>
Hello CodeSandbox
</h1>
<h2>{userRecipes.bookmarked_recipes}</h2>
</div>
);
}
const useRecipesData = () => {
const [recipes, setRecipes] = useState({});
const setBookmarks = newRecipes => {
setRecipes({
bookmarked_recipes: newRecipes,
});
};
useEffect(() => {
setRecipes({
bookmarked_recipes: "testtesttest",
});
}, []);
return [recipes, setBookmarks];
};

Resources