useState doesn't change simple state - reactjs

I need to set height of 'tbody' dynamically. That is why I use 'useRef' which watch to position of the element 'tfoot', and check 'tfoot.offsetTop' and change 'heightTbody' until the tbody is positioned all the way to the bottom. In 'positionChecker' I compare window.height and 'tfoot.offsetTop'. In the condition i try to change the state but unsuccesseble
What I do wrong?
Thanks
Component.tsx
.....several imports
const Component= () => {
const store = useContext(Context);
const [id, setId] = useState('');
const [heightTbody, setHeightTbody] = useState(650);
const checkVisibilityFoot = useRef<HTMLTableSectionElement>(null);
const [list, setList] = useState<ContentI[]>([]);
const fetchData = async () => {
const temp = await store.getContent();
setList(temp);
};
const resizeHolder = (e: UIEvent) => {
const w = e.target as Window;
if (!isNaN(w.innerHeight) && isFinite(w.innerHeight))
positionChecker(w.innerHeight, checkVisibilityFoot.current?.offsetTop as number);
};
const positionChecker = (windHeight: number, checkVisFoot: number) => {
if (windHeight < checkVisFoot) {
setHeightTbody((prevState) => prevState - 1); // <-- here must be changed
positionChecker(windHeight, checkVisibilityFoot.current?.offsetTop as number);
}
};
useEffect(() => {
fetchData();
}, []);
window.addEventListener('resize', resizeHolder);
return (
<table>
<tbody style={{ height: heightTbody + 'px' }}>
......content
</tbody>
<tfoot ref={checkVisibilityFoot}></tfoot>
</table>
)

If you want to watch for changes, pass params to useEffect array.

Related

React updates after two clicks instead of one

I'm creating a sortable table, and my issue is that the sorting shows after TWO clicks on a column, instead of just after one click.
This is my sorting logic: (DATA is hardcoded)
export const useFetch = (order) => {
const [data, setData] = useState();
const orderBy = order?.by || 'offer_id';
const fromTo = order?.fromTo || SORTING_ORDER.ascending;
useEffect(() => {
if (!orderBy || !fromTo) return;
let orderedData = DATA.sort((a, b) => (a[orderBy] - b[orderBy]));
setData(orderedData);
}, [orderBy, fromTo]);
return { data, status };
};
And I'm using this hook like this, from the component that has that table.
export const AcceptedOffers = ({ setModalIsOpen }) => {
const [order, setOrder] = useState({ by: 'maturity', fromTo: SORTING_ORDER.ascending });
const { data, status } = useFetch(order);
function onHeaderClick(header) {
setOrder({ by: header, fromTo: SORTING_ORDER.descending });
}
return (
<WidgetContainer>
<Title>
Accepted Offers
</Title>
<Table>
<Header>
<tr>
{
Object.entries(HEADERS).map(([key, value]) =>
<th
key={key}
onClick={() => onHeaderClick(key)}
>
{value}
</th>)
}
</tr>
</Header>
<Body>
{
data?.map(row => (<tr key={row.offer_id}>
<td>{row.offer_id}</td>
etc...
Can anyone explain what's wrong with this. Thank you.
The side effect represented by your useEffectis executed after the render triggered by the click: the data are rendered first, then sorted. That's where your "delay" commes from.
Here is an other solution. It may suit you, or not: the purpose is to show an alternative implementation to trigger the sort when order is modified, but without useEffect. It works by "overloading" setOrder:
export const useFetch = (initialOrder) => {
// useReducer may be a better choice here,
// to store order and data with a single state
// (and update this state through a single call)
const [order, setOrder] = useState(initialOrder);
const [data, setData] = useState();
const publicSetOrder = (newOrder) => {
setOrder(newOrder);
const orderBy = newOrder?.by || 'offer_id';
const fromTo = newOrder?.fromTo || SORTING_ORDER.ascending;
if (!orderBy || !fromTo) return;
let orderedData = DATA.sort((a, b) => (a[orderBy] - b[orderBy]));
setData(orderedData);
};
return { order, setOrder: publicSetOrder, data, status };
};
export const AcceptedOffers = ({ setModalIsOpen }) => {
const { order, setOrder, data, status } = useFetch({ by: 'maturity', fromTo: SORTING_ORDER.ascending });
function onHeaderClick(header) {
setOrder({ by: header, fromTo: SORTING_ORDER.descending });
}
// ...
Feel free to adapt to your use case ;)

component doesn't render after mergin two APIS

After I request two endpoints and store it in a new state variable I'm not being able to render the component after the state changes. When i assign the state variable to the dependency array of useEffect it renders infinitely.
I tried a few things but the only way that i've being able to do to render the component after it loads has been just adding the merge state to the dependency array.
import { ChangeEvent, FC, useEffect, useState } from "react";
import spacex from "../api/spacex";
import CardGrid from "../components/CardGrid";
import Header from "../components/Header";
import Pagination from "../components/Pagination";
import SkeletonGrid from "../components/SkeletonGrid";
type Launch = {
mission_name: string;
};
const LaunchesMain: FC = () => {
const [launches, setLaunches] = useState<any>([]);
const [rockets, setRockets] = useState<any>([]);
const [merged, setMerged] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [postsPerPage, setPostsPerPage] = useState(9);
const [searchTerm, setSearchTerm] = useState("");
const [filteredResult, setFilteredResult] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchRockets = async () => {
const responseRocket = await spacex.get("/rockets");
const responseLaunches = await spacex.get("/launches");
setRockets(responseRocket.data);
setLaunches(responseLaunches.data);
};
fetchRockets().then(() => {
const mergedApis = () => {
const launchesCopy: any = [...launches];
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launchesCopy[i].rocket = rockets[j];
}
}
}
setMerged(launchesCopy);
setIsLoading(false);
};
mergedApis();
});
}, []);
console.log(merged);
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
if (searchTerm.length === 0) {
setFilteredResult(merged);
} else if (searchTerm.length > 0) {
const filteredData = merged.filter((launch: Launch) => {
return `${launch.mission_name}`
.toLowerCase()
.includes(searchTerm.toLowerCase());
});
setFilteredResult(filteredData);
}
};
const lastPostIndex = currentPage * postsPerPage;
const firstPostIndex = lastPostIndex - postsPerPage;
const currentPosts = merged.slice(firstPostIndex, lastPostIndex);
return (
<>
<Header />
<div className="text-white">
<div>
<input
style={{
background:
"linear-gradient(0deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)), #121212",
}}
onChange={(event) => handleSearchChange(event)}
placeholder="Search all launches..."
value={searchTerm}
className="md:w-[26rem] w-[16rem] h-[3rem] rounded-lg mt-10 mx-5 md:mx-24 rounded-3"
/>
</div>
<div className="mx-5 md:ml-24 mt-5 opacity-40">
Total({currentPosts.length})
</div>
{isLoading ? (
<SkeletonGrid cards={postsPerPage} />
) : (
<CardGrid
postsData={currentPosts}
filteredResult={filteredResult}
searchTerm={searchTerm}
/>
)}
<Pagination
totalPosts={merged.length}
postsPerPage={postsPerPage}
setCurrentPage={setCurrentPage}
currentPage={currentPage}
/>
</div>
</>
);
};
export default LaunchesMain;
This is the code of the component. How can i solve this issue?
Since you need rockets and launches as a dependency of the useEffect, whenever they change, the useEffect is called, which calls the api, which changes, etc... However, you don't use rockets and launches states beyond merging them, and then you use the merged state.
So you don't have to store rockets and launches in the state. Use Promise.all() to get both data arrays in to .then() block, merge them, and store only the merged state:
useEffect(() => {
const fetchRockets = () => Promise.all(
spacex.get("/rockets"),
spacex.get("/launches")
])
fetchRockets()
.then(([responseRocket, responseLaunches]) => {
const rockets = responseRocket.data;
const launches = responseLaunches.data;
for (let i = 0; i < launches.length; i++) {
for (let j = 0; j < rockets.length; j++) {
if (launches[i].rocket.rocket_name === rockets[j].rocket_name) {
launches[i].rocket = rockets[j];
}
}
}
setMerged(launches);
setIsLoading(false);
});
}, []);

[solution]Why "Cards" still doesn't receive the passed value?

Why "Cards" still doesn't receive the passed value from selectedCountryInfo
I just tried passing await to the variable, still doesn't work. "Cards" still don't receive value.
<----solution: when there are have 2 setStates, should use 2 variables, not use 1 variable.(I guess if there are 3 setStates use 3 variables and so on)
I've been thinking about it for over 12 hours and can't think of a solution.
Because the default value of useState cannot put async/await.
(fetchedCountries is array,selectedCountryInfo is object)
const App = () => {
const [fetchedCountries, setFetchedCountries] = useState([]);
const [selectedCountryInfo, SetSelectedCountryInfo] = useState();
useEffect(() => {
const myCountries = async () => {
const countries = await worldWideCountries();
setFetchedCountries(countries);
SetSelectedCountryInfo(fetchedCountries[0]);
};
myCountries();
}, []);
return (
<div>
<Cards selectedCountryInfo={selectedCountryInfo} />
</div>
);
Solution:(from the 3 lines)
const countries = await worldWideCountries();
setFetchedCountries(countries);
const ww = countries[0];
SetSelectedCountryInfo(ww);
You probably want to use conditional rendering
const App = () => {
const [fetchedCountries, setFetchedCountries] = useState([]);
const [selectedCountryInfo, SetSelectedCountryInfo] = useState();
useEffect(() => {
const myCountries = async () => {
setFetchedCountries(await worldWideCountries());
SetSelectedCountryInfo(fetchedCountries[0]);
};
myCountries();
}, []);
return (
<div>
{ selectedCountryInfo && <Cards selectedCountryInfo={selectedCountryInfo} /> }
</div>
);
}

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 filter array with Hooks?

I am currently working on a class-based function. Trying to convert the class to a stateless function, followed by refactoring my code for each event handler from this.SetState to useState (in this case setMovies).
This is my code (partial code):
const Movies = () => {
const [movies, setMovies] = useState(getMovies());
const [disabled, setDisabled] = useState(true);
const [pageSize, setPageSize] = useState(4);
const sortBy = sortType => {
setMovies(movies.filter(sortType));
setDisabled(false);
// this.setState({
// movies: this.state.movies.sort(sortType),
// isDisabled: false,
// });
};
it seems that It is not possible to filter this state. I am able to change to boolean but can't filter my Array. Is there a way to filter using Hooks?
Thanks in advance.
Nothing changes...
const List = () => {
const [items, setItems] = useState([1, 2, 3, 4, 5, 6])
const filterEvenResults = () => setItems(items => items.filter(x => x % 2))
return (
<div>
{
items.map(item => <p key={item}>{item}</p>)
}
<button onClick={filterEvenResults}>Filter</button>
</div>
)
}

Resources