Unable to update react state with an array - reactjs

I can make a successful call to getApiSuggestions with data returned. However I'm unable to assign this to my state.
As you can see my console output shows that the value for response has an array. However, when attempting to assign it to wikiResults:response the array remains empty.
note that this is a modification of react-search-autocomplete
Am I attempting to pass the variables incorrectly?
NarrativeSearch.js
import React, {useContext, useState, useEffect} from "react";
import './search.css'
import { ReactSearchAutocomplete } from 'react-search-autocomplete'
import { getApiSuggestions } from '../../requests/requests';
import {TextSearchContext} from "../../contexts/TextSearchContext"
import {SearchContext} from "../../contexts/SearchContext"
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const [wikiTitleResults, setWikiTitleResults] = useState({wikiResults:[]});
var cnJson = wikiTitleResults;
const items = wikiTitleResults.wikiResults;
const handleOnSearch = (string, results) => {
console.log("STRING: ", string)
getApiSuggestions(string).then(response => {
console.log("RESPONSE: ", response);
setWikiTitleResults({wikiResults:response}); //<---- This doesn't update the state
console.log("WikiTitle: ", wikiTitleResults.wikiResults);
console.log("Items: ", items);
})
}
const handleOnHover = (result) => {
// the item hovered
console.log(result)
}
const handleOnSelect = (item) => {
// the item selected
setTextFilterState({textFilter:item.name});
console.log(item)
}
const handleOnFocus = () => {
console.log('Focused')
}
const handleOnClear = () => {
setTextFilterState({textFilter:""});
}
const formatResult = (item) => {
return (
<>
<span style={{ display: 'block', textAlign: 'left' }}>id: {item.title}</span>
</>
)
}
return (
<div >
<div className="searchbar">
<ReactSearchAutocomplete
items={items}
onSearch={handleOnSearch}
onHover={handleOnHover}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onClear={handleOnClear}
styling={{ zIndex: 4 }} // To display it on top of the search box below
autoFocus
/>
</div>
</div>
)
}
export default Search
getApiSuggesetions
const getApiSuggestions = (title) => {
//console.log("URL Being called"+ urlSingleResult);
//console.log(title);
let result = urlMultiResult
.get(`${title}`)
.then((response) => {
console.log(Object.values(response.data.query.pages))
return Object.values(response.data.query.pages);
})
.catch((error) => {
return error;
console.log(error);
});
console.log(result);
return result;
};

I fixed this by including a useEffect and a context from the parent component.
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const {wikiTitleResults, setWikiTitleResults} = useContext(SearchContext);
var items = wikiTitleResults.wikiTitles;
useEffect(() => {
const fetchData = async () => {
const data = await getApiSuggestions(textFilterState.textFilter)
setWikiTitleResults({wikiTitles:data})
}
fetchData();
},
[textFilterState])
const handleOnSearch = (string, results) => {
setTextFilterState({textFilter:string});
}

Related

How to avoid state reset while using Intersection observer in React.js?

I'm trying to implement intersection observer in react functional component.
import React, { useEffect, useRef, useState } from "react";
import { getData } from "./InfiniteClient";
export default function InfiniteScroll() {
const [data, setData] = useState([]);
const [pageCount, setPageCount] = useState(1);
const sentinal = useRef();
useEffect(() => {
const observer = new IntersectionObserver(intersectionCallback);
observer.observe(sentinal.current, { threshold: 1 });
getData(setData, data, pageCount, setPageCount);
}, []);
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => pageCount + 1);
getData(setData, data, pageCount, setPageCount);
}
};
return (
<section>
{data &&
data.map((photos, index) => {
return <img alt="" src={photos.url} key={index} />;
})}
<div className="sentinal" ref={sentinal}>
Hello
</div>
</section>
);
}
When I'm consoling prevCount above or prevData in the below function is coming as 1 and [] which is the default state.
function getData(setData, prevData, pageCount, setPageCount) {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${pageCount}&limit=10`
)
.then((val) => val.json())
.then((val) => {
console.log("prevD", prevData,val,pageCount);
if (!prevData.length) setData([...val]);
else {
console.log("Here", pageCount, prevData, "ddd", val);
setData([...prevData, ...val]);
}
}).catch((e)=>{
console.log("Error",e);
});
}
export { getData };
The code is not entering the catch block. I have also tried setPageCount(pageCount=> pageCount+ 1); and setPageCount(pageCount+ 1); gives same result. What am I doing wrong?
Code Sandbox
Edit: I converted the above code to class based component and it is working fine. I'm more curious on how hooks based approach is resets the states.
import React, { Component } from "react";
export default class InfiniteClass extends Component {
constructor() {
super();
this.state = {
pageCount: 1,
photos: []
};
}
getData = () => {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${this.state.pageCount}&limit=3`
)
.then((val) => val.json())
.then((val) => {
this.setState({
photos: [...this.state.photos, ...val],
pageCount: this.state.pageCount + 1
});
})
.catch((e) => {
console.log("Error", e);
});
};
componentDidMount() {
console.log(this.sentinal);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.getData();
}
});
observer.observe(this.sentinal, { threshold: 1 });
}
render() {
return (
<section>
{this.state &&
this.state.photos.length &&
this.state.photos.map((photo, index) => {
return <img alt="" src={photo.url} key={index} />;
})}
<div
className="sentinal"
ref={(sentinal) => (this.sentinal = sentinal)}
>
Hello
</div>
</section>
);
}
}
Edit 2 : I tried consoling pageCount at two places one above IntersectionCallback and one inside. The value inside is not changing meaning it is storing its own variables.
useState in react takes either argument or function. So, I did something hackish. It is working but I'm looking for a better aproach.
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => {
setData(d=>{
getData(setData, d, pageCount);
return d;
})
return pageCount + 1;
});
}
};

Still re-rendering using React.memo when the prop remain unchanged

I am creating a dropzone uploader for video-uploading (using react-dropzone-uploader package),
and also get its thumbnail with package ThumbnailExtractor.
I used React.Memo(base64) to ensure if the thumbnail's base64 remain unchanged, React will not re-render.
However, when I click submit button, the thumbnail keep re-rendering even the base64 remain unchanged.
Any clue?
import React, { useEffect, useState } from "react";
import axios from "axios";
import produce from "immer";
import Dropzone from "react-dropzone-uploader";
import ThumbnailContainer from "./ThumbnailContainer";
require("./dropzone.scss");
const DropZoneUploader = ({ api, setFiles }) => {
const [clonedFiles, setClonedFiles] = useState([]);
const UploadFileWithProgress = async (f, formData) => {
try {
const options = {
onUploadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
let percent = Math.floor((loaded * 100) / total);
console.log(`${loaded}kb of ${total}kb | ${percent}%`);
if (percent < 100) {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = percent;
})
);
} else {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = 99;
})
);
}
}
};
await axios
.post("https://reqres.in/api/users", formData, options)
.then(async (res) => {
console.log(res);
f.meta.percent = 100;
setClonedFiles(
produce((draft) => {
const newItem = draft.find((item) => item.meta.id === f.meta.id);
newItem.true_status = "uploaded";
})
);
setFiles(
produce((draft) => {
draft.unshift(res.data.data);
})
);
f.remove();
});
} catch (err) {
console.log(err);
return null;
}
};
// called every time a file's `status` changes
const handleChangeStatus = ({ meta, file }, status) => {
if (status === "done") {
setClonedFiles(
produce((draft) => {
draft.push({
file,
meta,
true_status: "ready"
});
})
);
}
if (status === "removed") {
setClonedFiles(
produce((draft) => {
const newItemIndex = draft.findIndex(
(item) => item.meta.id === meta.id && item.true_status === "ready"
);
if (newItemIndex !== -1) {
draft.splice(newItemIndex, 1);
}
})
);
}
};
// receives array of files that are done uploading when submit button is clicked
const handleSubmit = (files, allFiles) => {
files.map(async (f) => {
const formData = new FormData();
formData.append("title", `${f.meta.name}`);
formData.append("type", f.meta.type);
formData.append("file_purpose", "private");
formData.append("file", f.file);
const reee = await UploadFileWithProgress(f, formData);
if (reee) {
console.log("pushed");
}
});
};
const PreviewOutput = ({ files }) => {
const iconByFn = {
cancel: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
remove: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
restart: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
}
};
return (
<div className="dropzone__imgs-wrapper">
{clonedFiles?.map((f) => (
<div key={f.meta.id}>
<ThumbnailContainer f={f} files={files} />
</div>
))}
</div>
);
};
const MyCustomPreview = ({
input,
previews,
submitButton,
dropzoneProps,
files,
}) => (
<div className="dropzone">
<div {...dropzoneProps}>
<PreviewOutput files={files} />
{input}
</div>
{submitButton}
</div>
);
return (
<Dropzone
onChangeStatus={handleChangeStatus}
onSubmit={handleSubmit}
autoUpload={true}
accept="video/*"
LayoutComponent={(props) => <MyCustomPreview {...props} />}
/>
);
};
export default DropZoneUploader;
ThumbnailContainer.jsx
import React, { useEffect, useState } from "react";
import ThumbnailExtractor from "react-thumbnail-extractor";
import ThumbnailImageSrc from "./ThumbnailImageSrc";
const ThumbnailContainer = ({ f, files }) => {
const [vFile, setVFile] = useState();
const [vImage, setVImage] = useState();
const [isCaptured, setIsCaptured] = useState(false);
const fileFunction = files.find((a) => a.meta.id === f.meta.id);
const { name, previewUrl } = f.meta;
const { true_status, percent } = f;
useEffect(() => {
setVFile(f.file);
console.log("fFile render");
}, [f.file]);
return (
<div key={f.meta.id} className="dropzone__img">
{!previewUrl && (
<>
<ThumbnailExtractor
maxWidth={600}
videoFile={vFile}
count={1}
onCapture={(image) => {
if (!isCaptured) {
console.log("capture Render");
setVImage(image[0]);
setIsCaptured(true);
}
}}
/>
<span className="dzu-previewFileName">{name}</span>
</>
)}
<div className="dzu-previewStatusContainer">
{fileFunction &&
fileFunction.meta.percent !== 100 &&
f.true_status !== "uploaded" ? (
<progress max={100} value={percent} />
) : (
""
)}
{true_status === "ready" && (
<button
className="dropzone__img-delete"
type="button"
onClick={fileFunction?.remove}
>
<i className="las la-times"></i>
</button>
)}
{vImage && <ThumbnailImageSrc src={vImage} />}
</div>
</div>
);
};
export default ThumbnailContainer;
ThumbnailImageSrc.jsx (React.memo)
import React from "react";
export const ThumbnailImageSrc = React.memo(function ThumbnailImageSrc({
src
}) {
return (
<div
key={src}
className="dropzone_thumbnail-preview"
style={{ backgroundImage: `url(${src}` }}
></div>
);
});
export default ThumbnailImageSrc;
Here is the codesandbox sample.

[React-testing-library][FireEvent] Screen doesn't update after firing click event

I'm trying to simulate the 'see more' functionality to a blog.
It works as expected on the browser but when I simulate the behavior on react testing library it doesn't.
describe('when 12 blogs', () => {
describe('fetch more blogs', () => {
beforeEach(() => {
const twelveBlogs = generateBlogs(12);
const twoBlogs = generateBlogs(10);
Api.query.mockReturnValueOnce(twelveBlogs);
Api.query.mockReturnValueOnce(twoBlogs);
});
test('should fetch more blog posts when clicking on "See More" button', async () => {
render(
<MemoryRouter>
<Blog />
</MemoryRouter>
);
const seeMoreButton = await screen.findByRole('button', {
name: /See More/i,
});
fireEvent.click(seeMoreButton);
await waitFor(() => expect(Api.query).toHaveBeenCalledTimes(2));
await waitFor(
() =>
expect(screen.getAllByText(/NaN de undefined de NaN/)).toHaveLength(
15
)
);
});
});
});
And the implementation
import React from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Api from '../../api/api';
import BlogPreview from '../../components/BlogPreview/BlogPreview';
import './Blog.css';
function Blog() {
const [blogPosts, setBlogPosts] = useState([]);
const pageSize = 12;
const category = ['document.type', 'blog'];
const orderings = '[my.blog.data desc]';
const [apiPage, setApiPage] = useState(1);
const [shouldFetchMoreBlogs, setShouldFetchMoreBlogs] = useState(true);
useEffect(() => {
async function fetchApi(options) {
return Api.query(category, options);
}
const options = { pageSize, page: apiPage, orderings };
fetchApi(options).then((response) => {
if (response?.length > 0) {
if (blogPosts.length !== 0) {
setBlogPosts([...blogPosts, response]);
} else {
setBlogPosts(response);
}
} else {
setShouldFetchMoreBlogs(false);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiPage]);
async function handleSeeMoreClick() {
setApiPage(apiPage + 1);
}
function renderBlogPosts() {
if (blogPosts.length > 0) {
return blogPosts.map((blog, index) => (
<Link to={{ pathname: `/blog/${blog.uid}`, ...blog }} key={index}>
<BlogPreview key={index} {...blog} />
</Link>
));
}
}
function renderSeeMoreButton() {
debugger;
if (blogPosts.length > 0) {
if (blogPosts?.length % 12 === 0 && shouldFetchMoreBlogs) {
return (
<div className="see-more-container">
<button className="see-more-button" onClick={handleSeeMoreClick}>
Veja Mais
</button>
</div>
);
}
}
}
return (
<section className="content blog">
<h1>BLOG</h1>
<div className="blog-posts">{renderBlogPosts()}</div>
{renderSeeMoreButton()}
</section>
);
}
export default Blog;
It fails 'cause it only finds the initial 12 blog posts, even though it shows that the api was called twice.
There's obviously some async issue here.
I've tried switching from fireEvent to userEvent, from waitFor to find*, but it still doesn't work.
Thanks

useEffect only runs on hot reload

I have a parent/child component where when there is a swipe event occurring in the child the parent component should fetch a new profile. The problem is the useEffect in the child component to set up the eventListeneners currently is not running, only occasionally on hot-reload which in reality should run basically every time.
Child component
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const [swiped, setSwiped] = useState(0)
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
setSwiped((swiped) => swiped +1)
props.parentCallback(swiped);
startX = null
}
}
}
// Following code block does not work
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
Parent component
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback}/>
))}
</div>
);
}
}
If you want the parent components swiped state to change, you need to pass "setSwiped" from the parent to the child compenent. You will also need to pass "swiped" to the child to use its current value to calculate the new value. I'm going to assume you declared the useState in the child component trying to set the parents state of the same name, so I'm going to remove that useState Declaration in the child altogether.
Here's an example of passing the setSwiped method and swiped value to the child:
PARENT
import React, {useState, useEffect, useCallback} from 'react';
import './Index.css';
import Profile from './Profile'
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback} setSwiped={setSwiped} swiped={swiped}/>
))}
</div>
);
}
}
export default Profiles;
CHILD
import React, {useState, useRef, useEffect } from 'react';
import './Index.css';
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
props.setSwiped((props.swiped) => props.swiped +1)
props.parentCallback(props.swiped);
startX = null
}
}
}
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
export default Profile;
I'm hoping I didn't miss anything here.
Best of luck.

How to calculate shopping cart total

I have been working on a webpage for a while and am having trouble calculating the total for my shopping list products. I have created a "price" variable in another file, as well as a state for "quantity" in another file, and would like to be able to multiply these two variables together to calculate the "Total". The code for doing this, I have is as follows:
import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux';
import {
getCartItems,
removeCartItem,
onSuccessBuy
} from '../../../_actions/user_actions';
import UserCardBlock from './Sections/UserCardBlock';
import { Result, Empty } from 'antd';
import Axios from 'axios';
import Paypal from '../../utils/Paypal';
function CartPage(props) {
const dispatch = useDispatch();
const [Total, setTotal] = useState(0)
const [ShowTotal, setShowTotal] = useState(false)
const [ShowSuccess, setShowSuccess] = useState(false)
useEffect(() => {
let cartItems = [];
if (props.user.userData && props.user.userData.cart) {
if (props.user.userData.cart.length > 0) {
props.user.userData.cart.forEach(item => {
cartItems.push(item.id)
});
dispatch(getCartItems(cartItems, props.user.userData.cart))
.then((response) => {
if (response.payload.length > 0) {
calculateTotal(response.payload)
}
})
}
}
}, [props.user.userData])
const calculateTotal = (cartDetail) => {
let total = 0;
cartDetail.map(props => {
total += parseInt(props.productData.price, 10) * props.productData.price
});
setTotal(total)
setShowTotal(true)
}
const removeFromCart = (productId) => {
dispatch(removeCartItem(productId))
.then((response) => {
if (response.payload.cartDetail.length <= 0) {
setShowTotal(false)
} else {
calculateTotal(response.payload.cartDetail)
}
})
}
const transactionSuccess = (data) => {
dispatch(onSuccessBuy({
cartDetail: props.user.cartDetail,
paymentData: data
}))
.then(response => {
if (response.payload.success) {
setShowSuccess(true)
setShowTotal(false)
}
})
}
const transactionError = () => {
console.log('Paypal error')
}
const transactionCanceled = () => {
console.log('Transaction canceled')
}
return (
<div style={{ width: '85%', margin: '3rem auto' }}>
<h1>My Cart</h1>
<div>
<UserCardBlock
productData={props.location.state.data}
products={props.user.cartDetail}
removeItem={removeFromCart}
/>
<div style={{ marginTop: '3rem' }}>
<h2>Total amount: ${Total} </h2>
</div>
</div>
{/* Paypal Button */}
{ShowTotal &&
<Paypal
toPay={Total}
onSuccess={transactionSuccess}
transactionError={transactionError}
transactionCanceled={transactionCanceled}
/>
}
</div>
)
}
export default CartPage
Does this look correct? I am a little confused about what to put in the function before the arrows. I know I am doing something wrong... just not sure what. Any help is greatly appreciated. Thank you
the mapping look correct. are you concern about the mapping or the way you change the data? and maybe you can provide more code

Resources