Why element is not getting rendered and how can I fix it? - reactjs

I am trying to render some dynamic data in an element using useEffect Hook, which is not working.
Below is abc.tsx which has byTestOne(globalThis.test) which is responsible to send data back to us using globalThis.test value
const abc = () => {
const [data, setData] = useState<any>([]);
// globalThis.test is something that changes and when it changes, byTestOne triggers and get the data based on globalThis.test
useEffect(() => {
async function fetchData() {
const data = byTestOne(globalThis.test).then((data: any) => {
return data
})
setData(await data)
}
fetchData();
}, [globalThis.test]);
return (
<ImageBackground
source={require("../assets/images/abc.png")}
style={styles.bg}>
<View style={styles.containerHome}>
<CardStack>
{
// Below Element is not getting rendered
data.map((item: any) => (
<Card key={item.id}>
<CardItemForSwiper
name={item.name}
description={item.description}
/>
</Card>
)
)
}
</CardStack>
</View>
</ImageBackground>
);
};
export default ABC;
byTestOne() looks like this:
let data = [{
name: "test1",
description: "desc",
test: "one"
},
{
name: "test2",
description: "desc",
test: "two"
}]
export const byTestOne = (test: string) => {
const dataA = new Promise((resolve) => {
const filData = data.filter((rawData) => {
return rawData.test == test
})
if (filData.length > 0) {
globalThis.test = test
}
return resolve(filData)
})
return dataA
}

Related

Unable to use value of useRef in value of input in react native

I have a dropdown based on which different fields gets rendered. Initially I have initialized selected option of dropdown in useEffect hook, corresponding fields gets rendered and are working fine.
But when different option or even same option is chosen from dropdown fields gets rendered but when I type something in it it gets cleared. I have used useRef to store values for field at index i in fields array.
export default function CreateEntry({ navigation }) {
const { user, setLoading, setLoadingMsg } = useAuthContext();
const [name, setName] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [categoriesList, setCategoriesList] = useState([]);
const [primary, setPrimary] = useState("");
const [primaryValue, setPrimaryValue] = useState("");
const [fields, setFields] = useState([
{
name: "Email",
value: "",
type: "string",
encrypt: false,
},
{
name: "Password",
value: "",
type: "string",
encrypt: true,
},
]);
const refInputs = useRef([]);
useEffect(() => {
firestore()
.collection("categories")
.doc(user.uid)
.get()
.then((documentSnapshot) => {
setCategoriesList(documentSnapshot.data().category);
setSelectedCategory(documentSnapshot.data().category[0].name);
setFields(documentSnapshot.data().category[0].fields);
fields.map((field) => {
refInputs.current.push("");
});
})
.catch((error) => {
console.log(error);
});
}, []);
const addInput = () => {
refInputs.current.push("");
setFields([
...fields,
{ name: "First", value: "", type: "string", encrypt: false },
]);
};
const removeInput = (i) => {
refInputs.current[i] = "";
setFields((fields) =>
fields.map((field, index) => {
if (index === i) {
return null;
}
return field;
})
);
};
const setInputValue = (index, value) => {
refInputs.current[index] = value;
};
const inputs = [];
fields.forEach((field, i) => {
if (field !== null) {
inputs.push(
<View
key={i}
>
<TextInput
onChangeText={(value) => setInputValue(i, value)}
value={refInputs.current[i]}
/>
<TouchableOpacity
onPress={() => removeInput(i)}
>
<Ionicons name="close" style={[tw``]} size={22} color="#ddd" />
</TouchableOpacity>
</View>
);
}
});
const addEntry = () => {
//
};
return (
<ScrollView style={styles.container}>
<Dropdown
data={categoriesList}
value={selectedCategory}
onChange={(item) => {
refInputs.current = [];
setSelectedCategory(item.name);
setFields(item.fields);
item.fields.map((field) => {
refInputs.current.push("");
});
}}
/>
<Input
onChangeText={setName}
value={name}
placeholder="Name"
/>
{inputs}
<Pressable onPress={addInput}>
<Text}>+ Add a new input</Text>
</Pressable>
<MainButton onPress={addEntry}>Add</MainButton>
</ScrollView>
);
}

React mock asynchronous axios with jest doesn't work

I'm trying to test the component below using mock axios, however, it looks like the components are not rendered as expected, could someone help me on that? I have been stuck for quite a while. The component is fetching an api every 1 second.
const RealtimePrice = () => {
var [cryptoFeed, setCryptoFeed] = useState<cryptoFeed>([]);
var [currency, setCurrency] = useState(currencyList[0]);
var [cryptoSearch, setCryptoSearch] = useState("");
const url = `https://api.coingecko.com/api/v3/coins/markets?ids=${ids}&vs_currency=${currency}`;
const intervalRef = useRef<NodeJS.Timer>();
const onCurrencyChangeHandler = useCallback((newValue: string) => {
setCurrency(newValue);
}, []);
const onCryptoSearchChangeHandler = useCallback((newValue: string) => {
setCryptoSearch(newValue);
}, []);
useEffect(() => {
const getCryptoFeed = () => {
axios.get(url).then((response: any) => {
if (response.data) {
console.debug("The state is set");
setCryptoFeed(response.data);
} else {
console.debug("The state is not set");
setCryptoFeed([]);
}
});
};
getCryptoFeed();
intervalRef.current = setInterval(getCryptoFeed, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, [url]);
const priceBlocks = cryptoFeed
.filter((crypto) =>
crypto.name.toLowerCase().includes(cryptoSearch.toLowerCase())
)
.map((crypto: any) => {
return (
<PriceBlock
key={crypto.id}
id={crypto.id}
name={crypto.name}
price={crypto.current_price}
volume={crypto.total_volume}
change={crypto.price_change_24h}
></PriceBlock>
);
});
return (
<div className={styles.container}>
<div className={styles["header-section"]}>
<h1>Cryptocurrency Realtime Price</h1>
<div className="input-group">
<Selectbox
onChange={onCurrencyChangeHandler}
defaultOption={currencyList[0]}
options={currencyList}
/>
<Inputbox
placeHolder="Enter crypto name"
onChange={onCryptoSearchChangeHandler}
/>
</div>
</div>
<div className={styles.priceblocks}>{priceBlocks}</div>
</div>
);
};
The test is the defined as the following, findByText gives error, it couldn't find the element.
import { render, screen } from "#testing-library/react";
import RealtimePrice from "../RealtimePrice";
describe("Realtime Price", () => {
it("should render the Bitcoin price block", async () => {
render(<RealtimePrice />);
const pb = await screen.findByText("Bitcoin");
expect(pb).toBeInTheDocument();
});
});
And in package.json I have set
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
],
"resetMocks": false
}
In src/mocks/axios.js
const mockGetResponse = [
{
id: "bitcoin",
name: "Bitcoin",
price: 20000,
volume: 12004041094,
change: -12241,
},
{
id: "solana",
name: "Solana",
price: 87,
volume: 200876648,
change: 122,
},
];
const mockResponse = {
get: jest.fn().mockResolvedValue(mockGetResponse),
};
export default mockResponse;
With our comments seems clear the issue is that mock is not returning a proper response.data (that's why u are setting an empty array as the state)
Try doing:
const mockResponse = {
get: jest.fn().mockResolvedValue({data: mockGetResponse}),
};

Get data from API by map function

I'm running into a problem that I've been working on for days and unfortunately I can't figure it out by myself. I'm trying to create a View which shows some information from an API. But every time I map this item, I want to do another API call which checks the live price of that product.
So I have for example some JSON data what I get from an API.
{
"id": 1,
"name": "test product",
"productid": "73827duf"
},
{
"id": 2,
"name": "test product2",
"productid": "734437dde"
}
So I show this data with the following code inside my application:
{item.products.map((products) => {
return (
<View
key={products.id}
>
<Text
style={{
fontSize: FONTS.body3,
paddingLeft: 10,
}}
>
{products.name}
{getProductPriceJumbo(
products.productid
)}
</Text>
</View>
);
})}
So I want to run every time a function which fetches data from another API. I'm sending the productID because that's the only information I need to call this API. You can see this function down below:
function getProductPriceJumbo(id) {
fetch("https://---/test.php?id=" + id + "/", {
method: "GET",
})
.then((response) => response.json())
.then((data) => {
return data[0].price;
});
}
So this fetch returns a big list with information about the product from a third party API. I only want to return the price, that's the reason why I only return the price value and I want to print this out on the view above. I can't really figure out how to do this. I get undefined from the function every time I run it. Hope someone can help me with this.
Create a new Price Component to display the price
function Price({ id }) {
const [price, setPrice] = useState(0);
useEffect(() => {
function getProductPriceJumbo(id) {
fetch("https://---/test.php?id=" + id + "/", {
method: "GET"
})
.then((response) => response.json())
.then((data) => {
setPrice(data[0].price);
});
}
getProductPriceJumbo(id);
},[]);
return <Text>{price}</Text>;
}
And your .map will become
{
item.products.map((products) => {
return (
<View key={products.id}>
<Text
style={{
fontSize: FONTS.body3,
paddingLeft: 10
}}
>
{products.name}
<Price id={products.productid} />
</Text>
</View>
);
});
}
The reason you are getting undefined is because the window is rendering before the function finishes running. You will have define an asynchronous function before you return your view.
const [data, setData] = useState([])
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () =>{
setLoading(true);
try {
const {data: response} = await axios.get('API URL');
setData(response);
} catch (error) {
console.error(error.message);
}
setLoading(false);
}
fetchData();
}, []);
Then you can use data[0].price;
You'll probably want to make your individual product into its own component that handles the fetching, and setting the price to a state value that's local to that product view. Here's a full example of how you could do that:
import { useState, useEffect } from "react";
const Product = ({ product }) => {
const [price, setPrice] = useState("Price loading...");
useEffect(() => {
fetch("https://---/test.php?id=" + product.productid + "/", {
method: "GET"
})
.then((response) => response.json())
.then((data) => {
setPrice(data[0].price);
});
}, [product]);
return (
<View>
<Text
style={{
fontSize: FONTS.body3,
paddingLeft: 10
}}
>
{product.name}
{price}
</Text>
</View>
);
};
const App = () => {
const item = {
products: [
{
id: 1,
name: "test product",
productid: "73827duf"
},
{
id: 2,
name: "test product2",
productid: "734437dde"
}
]
};
return (
<div>
{item.products.map((product) => (
<Product key={product.id} product={product} />
))}
</div>
);
};
Alternatively, you could use Promise.all to get all of the price values before mapping your products:
import { useState, useEffect } from "react";
const App = () => {
const [item] = useState({
products: [
{
id: 1,
name: "test product",
productid: "73827duf"
},
{
id: 2,
name: "test product2",
productid: "734437dde"
}
]
});
const [products, setProducts] = useState([]);
useEffect(() => {
Promise.all(
item.products.map(async (product) => {
const response = await fetch(
`https://---/test.php?id=${product.productid}/`
);
const data = await response.json();
return {
...product,
price: data[0].price
};
})
).then((products) => setProducts(products));
}, [item]);
return (
<div>
{products.map((product) => {
return (
<View key={product.id}>
<Text
style={{
fontSize: FONTS.body3,
paddingLeft: 10
}}
>
{product.name}
{product.price}
</Text>
</View>
);
})}
</div>
);
};

How to use Infinite Scroll with FlatList and React Hook?

I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback

React hook logging a useState element as null when it is not

I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};

Resources