Why if statements not working as expected with RTK Query - reactjs

I'm trying to build an app which pulls data from backend with RTK Query and display that data as posts. I have successfully created RTKQ endpoint that gets data from backend with useEffect. However I want to put data in another array so that I can add more data with infinite scroll to that array but I am not able to do that.
My code is as follow. All help will be highly appreciated.
import { Box, Stack, Skeleton } from "#mui/material";
import React, { useEffect, useState } from "react";
import Post from "../components/postComponent";
import { useGetSimplesMutation } from '../services/authApi'
const Feed = () => {
const [getSimples, { isLoading, isSuccess, data }] = useGetSimplesMutation()
const [loadNow, setLoadNow] = useState(false)
let simplesData = []
useEffect(() => {
const handleGetSimples = async () => {
try {
await getSimples(0).unwrap()
if (isSuccess & !loadNow) {
console.log("Simples loaded")
simplesData = [...simplesData, ...data]
setLoadNow(true)
console.log(simplesData)
}
} catch (error) {
console.error(error.message)
}
}
handleGetSimples()
}, [])
return (
<Box flex={6} p={{ xs: 0, md: 2 }}>
{isLoading && (
<Box marginLeft={3}>
<Stack spacing={1} >
<Skeleton variant="text" height={100} />
<Skeleton variant="text" height={20} />
<Skeleton variant="text" height={20} />
<Skeleton variant="rectangular" height={300} />
</Stack>
</Box>
)}
{(isSuccess && loadNow) && simplesData.map(simples => {
return (
<div key={simples._id}>
<Post
userName={simples.userName}
dateSubmitted={simples.dateSubmitted}
beforePic={simples.beforePic}
afterPic={simples.afterPic}
tag={simples.tag}
/>
</div>
)
})}
</Box>
);
};
export default Feed;
This Code works when I directly map data obtained from RTKQ. However when I pass data to another array in an If statement the code inside if statement does not trigger when isSuccess gets true alongwith loadNow which has default state of false. I want to add data to simplesData array when this condition is true and then setLoadNow to true so that I can render my posts.

The line let simplesData = [] will be re-executed every time that the component re-renders, which will wipe out whatever you have stored.
If you want to keep data across multiple renders then you need to store it in a useState hook.
const [simplesData, setSimplesData] = useState([])
Inside your useEffect, you can call setSimplesData, using a functional update to minimize the dependencies of your effect.
setSimplesData(prevData => [...prevData, ...data])
There are some other issues here, mainly your lack of useEffect dependendencies. I think you want something like this?
const Feed = () => {
const [getSimples, { isLoading, isSuccess }] = useGetSimplesMutation()
const [loadPage, setLoadPage] = useState(0)
const [simplesData, setSimplesData] = useState([])
const handleEndReached = () => {
setLoadPage(prevPage => prevPage + 1);
}
const handleReset = () => {
setSimplesData([]);
setLoadPage(0);
}
useEffect(() => {
const handleGetSimples = async () => {
try {
const pageData = await getSimples(loadPage).unwrap()
console.log(`Simples page ${loadPage} loaded`)
setSimplesData(prevData => [...prevData, ...pageData])
} catch (error) {
console.error(error.message)
}
}
handleGetSimples()
}, [loadPage]);

Related

how to use useState like this? what does it mean?

Can anyone please explain me what const rerender = React.useState(0)[1] is this?
import React from 'react'
import axios from 'axios'
import {
useQuery,
useQueryClient,
QueryClient,
QueryClientProvider,
} from "#tanstack/react-query"
import { ReactQueryDevtools } from "#tanstack/react-query-devtools"
const getCharacters = async () => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get('https://rickandmortyapi.com/api/character/')
return data
}
const getCharacter = async (selectedChar) => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get(
`https://rickandmortyapi.com/api/character/${selectedChar}`,
)
return data
}
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const queryClient = useQueryClient()
**const rerender = React.useState(0)[1]**
const [selectedChar, setSelectedChar] = React.useState(1)
const charactersQuery = useQuery(['characters'], getCharacters)
const characterQuery = useQuery(['character', selectedChar], () =>
getCharacter(selectedChar),
)
return (
<div className="App">
<p>
Hovering over a character will prefetch it, and when it's been
prefetched it will turn <strong>bold</strong>. Clicking on a prefetched
character will show their stats below immediately.
</p>
<h2>Characters</h2>
{charactersQuery.isLoading ? (
'Loading...'
) : (
<>
<ul>
{charactersQuery.data?.results.map((char) => (
<li
key={char.id}
onClick={() => {
setSelectedChar(char.id)
}}
onMouseEnter={async () => {
await queryClient.prefetchQuery(
['character', char.id],
() => getCharacter(char.id),
{
staleTime: 10 * 1000, // only prefetch if older than 10 seconds
},
)
setTimeout(() => {
**rerender({})**
}, 1)
}}
>
<div
style={
queryClient.getQueryData(['character', char.id])
? {
fontWeight: 'bold',
}
: {}
}
>
{char.id} - {char.name}
</div>
</li>
))}
</ul>
<h3>Selected Character</h3>
{characterQuery.isLoading ? (
'Loading...'
) : (
<>
<pre>{JSON.stringify(characterQuery.data, null, 2)}</pre>
</>
)}
<ReactQueryDevtools initialIsOpen />
</>
)}
</div>
)
}
I want to know what it means, i am unable to understand this useState syntax I never seen this type of syntax. Can anyone share something about this?
useState has two parts the value and a function to update the value.
Take a look at the below snippet which explains how the useState hook is assigning values normally.
var fruitStateVariable = useState('banana'); // Returns a pair
var fruit = fruitStateVariable[0]; // The value of the state
var setFruit = fruitStateVariable[1]; // A asynchronous function to update the state.
By accessing the item with index 1 you are assigning rerender a function to update the state, which will trigger a rerender, as React sees the value going from 0 -> {}.
As to why the code is doing this, it seems the author is trying to get around useState being asynchronous to update.
This is not a good pattern and should be avoided!. As you rightly have said you have not seen this syntax before, because it is not an appropriate way of triggering a function to rerender.
React Docs recommends this way if you really need to force the retrigger:
const forceUpdate = useReducer(x => x + 1, 0)[1]
But a key line from this resource is to Try to avoid this pattern if possible.

React Context. how to avoid "Cannot read properties of undefined" error before having a value

I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length

Rerender component after function in provider ends

I'm trying to set components with 3 functionalities. Displaying PokemonList, getting random pokemon and find one by filters. Getting random pokemon works great but since 2 days I'm trying to figure out how to set pokemon list feature correctly
Below full code from this component.
It's render when click PokemonsList button inside separate navigation component and fire handleGetPokemonList function in provider using context.
The problem is that I can't manage rerender components when PokemonList is ready. For now i need to additionally fire forceUpadte() function manually (button onClick = () => forceUpdate())
I tried to use useEffect() in PokemonList component but it didn't work in any way.
I was also sure that after fetching data with fetchData() function I can do .then(changeState of loading) but it didn't work also.
What Am I missing to automatically render data from fetch in provider in PokemonList component? I'm receiving error about no data exist but if I use forceUpdate then everything is ok
Complete repo here: https://github.com/Mankowski92/poke-trainer
handleGetPokemonList function in provider below
const handleGetPokemonList = () => {
setCurrentPokedexOption('pokemonList');
async function fetchData() {
setImgLoaded(false);
let res = await fetch(`${API}?offset=0&limit=6/`);
let response = await res.json();
response.results.forEach((item) => {
const fetchDeeper = async () => {
let res = await fetch(`${item.url}`);
let response = await res.json();
let eachPoke = {
id: response.id,
name: response.name,
artwork: response.sprites.other['officialartwork'].front_default,
stats: response.stats,
};
fetchedPokemons.push(eachPoke);
};
fetchDeeper();
});
setPokemonList(fetchedPokemons);
if (fetchedPokemons) {
return setLoading(false);
}
}
fetchData()
.then((res) => setLoading(res))
.catch((err) => console.log('error', err));
};
PokemonList component below
import React, { useContext, useState, useCallback } from 'react';
import { StyledPokemonListContainer } from './PokemonList.styles';
import { PokemonsContext } from '../../../providers/PokemonsProvider';
const PokemonList = () => {
const ctx = useContext(PokemonsContext);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const { handleSetImgLoaded } = useContext(PokemonsContext);
return (
<>
{ctx.currentPokedexOption === 'pokemonList' ? (
<StyledPokemonListContainer>
{ctx.pokemonList && ctx.pokemonList.length ? (
ctx.pokemonList.map((item, i) => (
<div className="each-pokemon-container" key={i}>
<div className="poke-id">{item.id}</div>
<div className="poke-name">{item.name}</div>
<img className="poke-photo" onLoad={() => handleSetImgLoaded()} src={item ? item.artwork : ''} alt="" />
</div>
))
) : (
<div className="render-info">Hit rerender button</div>
)}
{/* {ctx.pokemonList ? <div>{ctx.pokemonList[0].name}</div> : <div>DUPPSKO</div>} */}
<div className="buttons">
<button onClick={() => console.log('PREVOIUS')}>Previous</button>
<button className="rerender-button" onClick={() => forceUpdate()}>
RERENDER
</button>
<button onClick={() => console.log('NEXT')}>Next</button>
</div>
</StyledPokemonListContainer>
) : null}
</>
);
};
export default PokemonList;

useSWR conditional fetch and react-boostrap Accordion

trying to load youtube comments into a infinite load component (using a npm for it)
the mess happens due to the fact the infinite load component is child of parent Accordion component (from react-bootstrap), and what I'm trying to achieve is fetching with useSWR only if Accordion gets clicked (opened).
What I tried is to use useSWR conditional, so that only fetches when state "show" is true, which is being set inside function:
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
called on Accordion.Toggle onClick event.
But what happens is I can only show the comments after I click the Accordion twice, why is that?
My code is:
import { useState, useEffect } from 'react'
import { Row, Col, Button, Accordion } from 'react-bootstrap'
import * as _ from 'lodash'
import useSWR from 'swr'
import { MdUnfoldMore } from 'react-icons/md'
import InfiniteScroll from "react-infinite-scroll-component"
import Comments from './Comments'
const siteurl = process.env.NEXT_PUBLIC_SITE_URL
export default function VideoComments({ video }){
const [show, setShow] = useState(false)
const [counter, setCounter] = useState(0)
const [commList, setCommList] = useState(null)
const [commChunks, setCommChunks] = useState([])
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
const fetcher = (...args) => fetch(...args).then(res => res.json())
const { data: comments, error } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)
// useEffect(() => {
// if (comments) {
// commChunks = _.chunk(comments.comm, 10)
// setCommList(commChunks[counter])
// }
// },[comments])
const fetchMoreData = () => {
const newCounter = counter + 1;
// loaded all, return
if (commChunks[newCounter] === undefined || commChunks[newCounter] == null) {
return;
}
const newCommList = [
...commList,
...commChunks[newCounter]
]
setCommList(newCommList)
setCounter(newCounter)
}
return (
<div>
<Accordion>
<Row>
<Col xs={12}>
<Accordion.Toggle as={Button} onClick={() => {showComments()}} variant="link" eventKey="0"><div><span>Comments</span></div></Accordion.Toggle>
</Col>
</Row>
<Accordion.Collapse eventKey="0">
<div id="commentsBox" style={{maxHeight: '300px', overflowY: 'auto'}}>
<Col xs={12}>
{commList &&
<InfiniteScroll
dataLength={commList.length}
next={fetchMoreData}
hasMore={true}
scrollableTarget="commentsBox"
>
<Comments data={commList} />
</InfiniteScroll>
}
</Col>
</div>
</Accordion.Collapse>
</Accordion>
</div>
);
}
EDIT: as suggested below I reactivated useEffect, but it still needs two clicks of the Accordion
const showComments = () => {
setShow(true)
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
}
const { data: comments } = useSWR(show ? `${siteurl}/api/c/${video.id}` : null, fetcher)
useEffect(() => {
if (comments) {
setCommChunks(_.chunk(comments.comm, 10))
setCommList(commChunks[counter])
}
},[comments])
The issue is in your useEffect, calling setCommList(commChunks[counter]) right after modifying commChunks state won't have the updated value. Setting state in React is an asynchronous operation (see React setState not updating immediately).
You should save the comments in a block-scoped variable and use that to update both states consecutively.
useEffect(() => {
if (comments) {
const commentsChunks = _.chunk(comments.comm, 10)
setCommChunks(commentsChunks)
setCommList(commentsChunks[counter])
}
}, [comments])
You commented the useEffect that handles the comments :
// useEffect(() => {
// if (comments) {
// commChunks = _.chunk(comments.comm, 10)
// setCommList(commChunks[counter])
// }
// },[comments])
What happens :
You click the Accordion, showComments is called
show is set to true, but because comments is undefined, commList and commChunks are not set
the component re-renders, now useSWR can use the url to fetch data
the component re-renders when the fetching si done, now comments contains the data
You click the Accordion the second time, showComments is called
show is set to true, this time commList and commChunks are set
the component re-renders with InfiniteScroll and Comments

Memorize fetched results in component

I'm working on a component that adds images to items. You can either upload your own image or pick an image, loaded from an API based on the name of the item.
Here is the root component:
const AddMedia = (props) => {
const [currentTab, setCurrentTab] = useState(0);
const [itemName, setItemName] = useState(props.itemName);
return (
<div>
<Tabs
value={currentTab}
onChange={() => setCurrentTab(currentTab === 0 ? 1 : 0)}
/>
<div hidden={currentTab !== 0}>
<FileUpload />
</div>
<div hidden={currentTab !== 1}>
{currentTab === 1 && <ImagePicker searchTerm={itemName} />}
</div>
</div>
);
};
And here is the <ImagePicker />:
import React, { useState, useEffect } from "react";
function ImagePicker({ searchTerm, ...props }) {
const [photos, setPhotos] = useState([]);
const searchForImages = async (keyword) => {
const images = await api.GetImagesByKeyword(keyword);
return images;
};
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, []);
return (
<>
{photos.map(({ urls: { small } }, j) => (
<img alt={j} src={small} className={classes.img} />
))}
</>
);
}
const areSearchTermsEqual = (prev, next) => {
return prev.searchTerm === next.searchTerm;
};
const MemorizedImagePicker = React.memo(ImagePicker, areSearchTermsEqual);
export default MemorizedImagePicker;
What I'm struggling with is getting the component to not fetch the results again if the searchTerm hasn't changed. For example, when the component loads it's on tab 0 (upload image), you switch to tab 1 (pick an image) and it fetches the results for searchTerm, then you switch to 0 and again to 1 and it fetches them again, although the searchTerm hasn't changed. As you can see, I tried using React.memo but to no avail. Also, I added the currentTab === 1 to stop it from fetching the photos when the root component renders and fetch them only if the active tab is 1.
You should add the searchTerm as dependency of the useEffect so that it will not fetch again if searchTerm hasn't change:
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, [searchTerm]);
Additional information, if you are using eslint to lint your code, you can use the react-hooks/exhaustive-deps rule to avoid this kind of mistake.

Resources