I've got a component, Crafts.js, which calls for useFetch() with a firestore query as an argument:
const q = query(craftsColRef, where("category", "==", currPage));
const { data:crafts, setData:setCrafts, mats, setMats } = useFetch(q);
The third argument of where() in the query is a prop passed to crafts component which is updated in a parent component. The currPage prop does update with the new value, but I think it's clear that React doesn't call useFetch on re-render, therefore I don't get the new data.
I'm trying to achieve some kind of navigation. User clicks a button and the docs are filtered in a different way. How can I achieve this?
Thank you!
I am not sure what is written in your useFetch but you can write your custom hook and from my understanding of your logic flow, I made a sample code. Hope it helps
import { useEffect, useState } from "react";
function firestoreQuery(currPage) {
// since this is just a fake api
// please change it to your query logic here
return new Promise(resolve => {
resolve([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].sort(() => Math.random() - 0.5));
})
}
// Your custom hook
function useCustomFetch(currPage) {
const [items, setItems] = useState([]);
async function fetch() {
let result = await firestoreQuery(currPage);
setItems(result);
}
useEffect(() => {
if (!currPage) return;
console.log("fetch")
fetch();
}, [currPage]);
return { items };
}
function Craft({ currPage }) {
const { items } = useCustomFetch(currPage);
return <div>
{items.map(i => <span>{i}</span>)}
</div>
}
function ParentComponentPage() {
const [timestamp, setTimestamp] = useState();
return <div>
<button onClick={() => setTimestamp(new Date().getTime())}>Change currPage</button>
<Craft currPage={timestamp} />
</div>
}
export default ParentComponentPage;
Related
I'm retrieving some data from an API using useEffect, and I want to be able to filter that returned data using a prop being fed into the component from its parent.
I'm trying to filter the state after it is set by useEffect, however it looks like the component is going into an infinite render loop.
What do I need to do to prevent this?
export default function HomeJobList(props: Props): ReactElement {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
return (
<>
<div>do stuff</div>
</>
);
}
I understand that the use of the setListing function is then causing a rerender after the filtering, which then causes another setListing call. But what's the best way to break this loop?
Should I just have another state value that maintains the last searchTerm used to filter and check against that before filtering?
Or is there a better way?
It's an indinite loop because every time you filter, you set it as a state variable, which causes re-rendering and filtering & setting the variable again - thus a loop.
I suggest you do it all in one place (your useEffect is a good place for that, because it only executes once.
useEffect(() => {
(async () => {
const res = await service.getListings();
const filtered = res.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
})();
}, []);
When the state changes that trigger a re-render, that's why you have an infinite loop; what you have to do is to wrap your filtering in a useEffectthat that depends on the searchTerm prop, like this:
import React, { useEffect } from 'react';
export default function HomeJobList() {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
useEffect(() => {
if (listings) {
let filtered = listings.filter(x =>
x.positionTitle.includes(props.searchTerm)
);
setListings(filtered);
}
}, [props.searchTerm]);
return (
<>
<div>do stuff</div>
</>
);
}
You need to create a function that's called inside the JSX you're returning.
Actually, you'll need to render the component every time the Props objects changes. That's achieved by calling the function in the JSX code.
Example:
Function:
const filteredListings = () => {
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => {
x.positionTitle.includes(props.searchTerm));
}
return filtered;
}
}
Return Statement:
return (
<ul>
{
filteredListings().map((listing) =>
<li>{listing.title}</li>
);
}
</ul>
);
What you need is a useEffect which does the filtering when props changes.
Replace this
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
with
useEffect(()=>{
if (props.searchTerm) {
setListings(prevListing => {
return prevListing.filter((x) => x.positionTitle.includes(props.searchTerm))
});
}
}, [props.searchTerm] )
I need the useProductList function to execute and finish all process before randomProduct function will execute.
For some reason it doesnt work when fetchData is a Promise so randomProduct wont be executed.
I even tried without Promise, nothing did work.
my custom hook
import { useState, useEffect } from "react";
export default function useProductList() {
const [productList, setProductObjsList] = useState([]);
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
let randomProducts = [];
const fetchData = () =>
new Promise(() => {
arr.splice(1, 7);
setProductObjsList(arr);
return arr;
});
const randomProduct = (productArr) => {
//some Math.random() and algorithm with the productArr
console.log("randomProduct()", productArr);
};
useEffect(() => {
fetchData().then((result) => randomProduct(result));
}, []);
return randomProducts;
}
CodeSandbox
I will be glad if someone will open my eyes and show me the right way of how to do it.
EDIT:
My original fetchData function
const fetchData = () =>
{
fetch('http://localhost:49573/WebService.asmx/ProductList')
.then((response) => response.text())
.then(
(xml) =>
new window.DOMParser().parseFromString(xml, 'text/xml')
.documentElement.firstChild.textContent
)
.then((jsonStr) => JSON.parse(jsonStr))
.then((data) => {
setProductObjsList(data);
})
});
randomProduct function
```const randomProduct = (productObjectList) => {
const products = [...productObjectList];
for (let index = 0; index < products.length; index++) {
let idx = Math.floor(Math.random() * products.length);
randomProducts.push(products[idx]);
products.splice(idx, 1);
}
};```
It's unclear what your exact intentions are, but from the name useRandomProduct I'm going to make a guess. Top things I'd like for you to take from this answer -
You cannot mutate state in React. You cannot use Array.prototype.pop and expect your React components to work correctly. If a value is meant to change over the lifetime of the component, use useState.
Don't put all of your functions inside of the hook. This tendency probably comes from class-oriented thinking but has no place in functional paradigm. Functions can be simple and do just one thing.
import { useState, useEffect } from "react"
// mock products
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
// mock fetch
const fetchData = () =>
new Promise(resolve => {
resolve(arr)
})
// random picker
function choose1(all) {
const i = Math.floor(Math.random() * all.length)
return all[i]
}
export default function useRandomProduct() {
// state
const [product, setProduct] = useState(null)
// effect
useEffect(async () => {
// fetch products
const products = await fetchData()
// then choose one
setProduct(choose1(products))
}, [])
// return state
return product
}
To use your new hook -
import { useRandomProduct } from "./useRandomProduct.js"
import Product from "./Product.js"
function MyComponent() {
const product = useRandomProduct()
if (product == null)
return <p>Loading..</p>
else
return <Product {...product} />
}
Full demo -
const { useState, useEffect } = React
// useRandomProduct.js
const arr = [{name:"apple"},{name:"carrot"},{name:"pear"},{name:"banana"}]
const fetchData = () =>
new Promise(r => setTimeout(r, 2000, arr))
function choose1(all) {
const i = Math.floor(Math.random() * all.length)
return all[i]
}
function useRandomProduct() {
const [product, setProduct] = useState(null)
useEffect(() => {
fetchData().then(products =>
setProduct(choose1(products))
)
}, [])
return product
}
// MyComponent.js
function MyComponent() {
const product = useRandomProduct()
if (product == null)
return <p>Loading..</p>
else
return <div>{JSON.stringify(product)}</div>
}
// index.js
ReactDOM.render(<div>
<MyComponent />
<MyComponent />
<MyComponent />
</div>, document.querySelector("#main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="main"></div>
The problem is you are not resolving the promise. So, until the promise is resolved it will not go inside then() block.
Here's what you have to do
const fetchData = () =>
new Promise((resolve,reject) => {
arr.pop(1, 7);
setProductObjsList(arr);
resolve(arr);
});
A promise has three stage.
Pending
Resolved
Rejected
In your example the promise is always in pending state and never goes to the Resolved state. Once, a promise is resolved it moves to then() block and if it is rejected it moves to catch() block.
new to react so I am not quite sure what I am doing wrong here... I am trying to call data from an API, then use this data to populate a charts.js based component. When I cmd + s, the API data is called in the console, but if I refresh I get 'Undefined'.
I know I am missing some key understanding about the useEffect hook here, but i just cant figure it out? All I want is to be able to access the array data in my component, so I can push the required values to an array... ive commented out my attempt at the for loop too..
Any advice would be greatly appreciated! My not so functional code below:
import React, {useState, useEffect} from 'react'
import {Pie} from 'react-chartjs-2'
const Piegraph = () => {
const [chartData, setChartData] = useState();
const [apiValue, setApiValue] = useState();
useEffect(async() => {
const response = await fetch('https://api.spacexdata.com/v4/launches/past');
const data = await response.json();
const item = data.results;
setApiValue(item);
chart();
},[]);
const chart = () => {
console.log(apiValue);
const success = [];
const failure = [];
// for(var i = 0; i < apiValue.length; i++){
// if(apiValue[i].success === true){
// success.push("success");
// } else if (apiValue[i].success === false){
// failure.push("failure");
// }
// }
var chartSuccess = success.length;
var chartFail = failure.length;
setChartData({
labels: ['Success', 'Fail'],
datasets: [
{
label: 'Space X Launch Statistics',
data: [chartSuccess, chartFail],
backgroundColor: ['rgba(75,192,192,0.6)'],
borderWidth: 4
}
]
})
}
return (
<div className="chart_item" >
<Pie data={chartData} />
</div>
);
}
export default Piegraph;
There are a couple issues that need sorting out here. First, you can't pass an async function directly to the useEffect hook. Instead, define the async function inside the hook's callback and call it immediately.
Next, chartData is entirely derived from the apiCall, so you can make that derived rather than being its own state variable.
import React, { useState, useEffect } from "react";
import { Pie } from "react-chartjs-2";
const Piegraph = () => {
const [apiValue, setApiValue] = useState([]);
useEffect(() => {
async function loadData() {
const response = await fetch(
"https://api.spacexdata.com/v4/launches/past"
);
const data = await response.json();
const item = data.results;
setApiValue(item);
}
loadData();
}, []);
const success = apiValue.filter((v) => v.success);
const failure = apiValue.filter((v) => !v.success);
const chartSuccess = success.length;
const chartFail = failure.length;
const chartData = {
labels: ["Success", "Fail"],
datasets: [
{
label: "Space X Launch Statistics",
data: [chartSuccess, chartFail],
backgroundColor: ["rgba(75,192,192,0.6)"],
borderWidth: 4,
},
],
};
return (
<div className="chart_item">
<Pie data={chartData} />
</div>
);
};
export default Piegraph;
pull your chart algorithm outside or send item in. Like this
useEffect(async() => {
...
// everything is good here
chart(item)
})
you might wonder why I need to send it in. Because inside useEffect, your apiValue isn't updated to the new value yet.
And if you put the console.log outside of chart().
console.log(apiData)
const chart = () => {
}
you'll get the value to be latest :) amazing ?
A quick explanation is that, the Piegraph is called whenever a state is updated. But this update happens a bit late in the next cycle. So the value won't be latest within useEffect.
I'm trying to create a functional component that fetches data from an API and renders it to a list. After the data is fetched and rendered I want to check if the URL id and list item is equal, if they are then the list item should be scrolled into view.
Below is my code:
import React, { Fragment, useState, useEffect, useRef } from "react";
export default function ListComponent(props) {
const scrollTarget = useRef();
const [items, setItems] = useState([]);
const [scrollTargetItemId, setScrollTargetItemId] = useState("");
useEffect(() => {
const fetchData = async () => {
let response = await fetch("someurl").then((res) => res.json());
setItems(response);
};
fetchData();
if (props.targetId) {
setScrollTargetItemId(props.targetId)
}
if (scrollTarget.current) {
window.scrollTo(0, scrollTarget.current.offsetTop)
}
}, [props]);
let itemsToRender = [];
itemsToRender = reports.map((report) => {
return (
<li
key={report._id}
ref={item._id === scrollTargetItemId ? scrollTarget : null}
>
{item.payload}
</li>
);
});
return (
<Fragment>
<ul>{itemsToRender}</ul>
</Fragment>
);
}
My problem here is that scrollTarget.current is always undefined. Please advice what I'm doing wrong. Thanks in advance.
Using useCallback, as #yagiro suggested, did the trick!
My code ended up like this:
const scroll = useCallback(node => {
if (node !== null) {
window.scrollTo({
top: node.getBoundingClientRect().top,
behavior: "smooth"
})
}
}, []);
And then I just conditionally set the ref={scroll} on the node that you want to scroll to.
That is because when a reference is changed, it does not cause a re-render.
From React's docs: https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
constructor(props) {
thi.modal = React.createRef();
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.modal.current.openModal('signup') // it will call function of child component of Modal
});
// debugger
}
return(
<>
<button className="login-button" onClick={this.handleSwitch}>Log in with email</button>
<Modal ref={this.modal} />
</>
)
React Hooks will delay the scrolling until the page is ready:
useEffect(() => {
const element = document.getElementById('id')
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [])
If the element is dynamic and based on a variable, add them to the Effect hook:
const [variable, setVariable] = useState()
const id = 'id'
useEffect(() => {
const element = document.getElementById(id)
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [variable])
i want to pass the string from asyncstorage to a state variable using hooks, but it doesn't work, trying for hours but still doesn't work.
here is the code:
const [cartitems, cartitemsfunc] = useState('');
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
console.log('items from cart2:' + cartitems);
};
d();
when i try console logging cartitems it logs an empty string in the console,but items logs out the string
can someone please tell me where i went wrong
thanks in advance!!!
As mentioned in the comments and link supplied, useState is asynchronous so setting a value and immediately reading it in the following line will not yield consistent result:
cartitemsfunc(items); // async call
console.log('items from cart2:' + cartitems); // cartitems not updated yet
It is important to also understand that whenever you update state using useState, the component will render again. If you have a method call in the body of the app it will be run everytime you update state. So what you have is a scenario where you are making a call to update state, but the method is being executed and ends up overwriting your changes.
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
console.log('items from cart2:' + cartitems);
};
d(); // this will run every time the component renders - after you update state
If you only need the value at initial render, then call the method from useEffect and set the dependency chain to [] so it only runs once at first render, and not every time state is updated.
Below is an example that demonstrates getting/setting values from localStorage and also updating the state directly.
CodeSandbox Demo
import React, { useState, useEffect } from "react";
import AsyncStorage from "#react-native-community/async-storage";
import "./styles.css";
export default function App() {
const [cartItems, setCartItems] = useState(null);
const setLSItems = async () => {
await AsyncStorage.setItem(
"items",
JSON.stringify([{ id: "foo", quantity: 1 }, { id: "bar", quantity: 2 }])
);
getLSItems(); // or setCartItems("Foo");
};
const clearLSItems = async () => {
await AsyncStorage.removeItem("items");
getLSItems(); // or or setCartItems(null);
};
const getLSItems = async () => {
const items = await AsyncStorage.getItem("items");
setCartItems(JSON.parse(items));
};
// bypass using local storage
const setCartItemsDirectly = () => {
setCartItems([{ id: "baz", quantity: 3 }]);
};
useEffect(() => {
getLSItems();
}, []); // run once at start
return (
<div className="App">
<div>
<button onClick={setLSItems}>Set LS Items</button>
<button onClick={clearLSItems}>Clear LS Items</button>
</div>
<div>
<button onClick={setCartItemsDirectly}>Set Cart Items Directly</button>
</div>
<hr />
{cartItems &&
cartItems.map((item, index) => (
<div key={index}>
item: {item.id} | quantity: {item.quantity}
</div>
))}
</div>
);
}
Since setState is async you will need to observe when it finishes, this can be easily done using useEffect and adding caritems to the dependencies array
const [cartitems, cartitemsfunc] = useState('');
const d = async () => {
let items = await AsyncStorage.getItem('items');
console.log('items from cart:' + items);
cartitemsfunc(items);
};
useEffect(()=> {
console.log('items from cart2:' + cartitems);
}, [caritems]);
d();
I think here console.log() statement is executed before updating the state. As here await has been used so it will execute the next lines before getting the result.
Therefore , in this type of situation we use combination of useEffect() and useState(). useEffect() for getting the data and useState() for updating the state and re-render the UI.