How to set order of functions execution inside useEffect hook - reactjs

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.

Related

Unable to update array with Min and Max values

I'm writing a code where I need to get Min and Max values. In my real-time scenario, I've got an SDK to which I make two promise calls and it returns min and max values. I'm trying to mock up the same in my example below. I've got a context in place that is used to store the values. Here is my sample code.
Here is my context:
import React, { useState } from "react";
import { useContext } from "react";
const ProductsContext = React.createContext();
export const ProductsProvider = ({ children }) => {
const [priceValues, setPriceValues] = useState([0, 0]);
return (
<ProductsContext.Provider
value={{
priceValues,
setPriceValues
}}
>
{children}
</ProductsContext.Provider>
);
};
export const useProductsContext = () => {
return useContext(ProductsContext);
};
And my code is as below:
import { useEffect } from "react";
import { PriceRange } from "./PriceRange";
import { useProductsContext } from "./ProductsContext";
const ShowRange = () => {
const { priceValues, setPriceValues } = useProductsContext();
const getMinVal = () => {
setPriceValues([Math.random() * 100, priceValues[1]]);
};
const getMaxVal = () => {
setPriceValues([priceValues[0], Math.random() * 100]);
};
useEffect(() => {
getMinVal();
getMaxVal();
}, []);
console.log(JSON.stringify(priceValues));
return <>{priceValues[0] && priceValues[1] && <PriceRange />}</>;
};
export default ShowRange;
Currently, when I run this, priceValues is of format [0, randomNumber]. And once I get the values.
I'm confused on why only the second value in Array gets updated but not the first. Where am I going wrong?
Here is working code of the same.
Even though getMinVal and getMaxVal are separate functions that update state, they are called at the same time. This is causing the first update to be lost by the second update since priceValues[1] will not be updated until the next render.
You could solve this by using the function update form of setting state:
const getMinVal = () => {
setPriceValues((prev) => ([Math.random() * 100, prev[1]]));
};
const getMaxVal = () => {
setPriceValues((prev) => ([prev[0], Math.random() * 100]));
};
After you call setPriceValues React sets value (internally) and schedule re-render, but value stored in priceValues const is not updated.
State is not exactly like variable. State has same value during one re-render. If you want actual value during re-render (after mutation) pass function with param to setState.
You code with expecting behaviour:
const ShowRange = () => {
const { priceValues, setPriceValues } = useProductsContext(); // priceValues == [0, 0]
const getMinVal = () => {
// priceValues == [0, 0]
setPriceValues((pricesValues) => [Math.random() * 100, priceValues[1]]);
};
const getMaxVal = () => {
// priceValues == [0, 0]
setPriceValues((priceValues) => [priceValues[0], Math.random() * 100]);
};
useEffect(() => {
// priceValues == [0, 0]
getMinVal();
// priceValues == [0, 0]
getMaxVal();
}, []);
console.log(JSON.stringify(priceValues));
return <>{priceValues[0] && priceValues[1] && <h1>Hi</h1>}</>;
};

How to call custom hook useFetch with different query per render?

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;

React State value not updated in Arrow functional component

React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);

React Sequential Rendering Hook

I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};

How to get value from useState inside the function

I am trying to build Hanging man game and want to get value from useState inside the checkMatchLetter function, but not sure if that is possible and what I did wrong....
import React, { useState, useEffect } from 'react';
import { fetchButton } from '../actions';
import axios from 'axios';
import 'babel-polyfill';
const App = () => {
const [word, setWord] = useState([]);
const [underscore, setUnderscore] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const runEffect = async () => {
const result = await axios('src/api/api.js');
setData(result.data)
}
runEffect();
}, []);
const randomWord = () => {
const chosenWord = data[Math.floor(Math.random() * data.length)];
replaceLetter(chosenWord.word);
}
const replaceLetter = (string) => {
let getString = string; // here it shows a valid string.
setWord(getString);
let stringToUnderScore = getString.replace(/[a-z]/gi, '_');
setUnderscore(stringToUnderScore);
}
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, []);
const checkMatchLetter = (keyButton) => {
console.log(keyButton);
let wordLength = word.length;
console.log(wordLength); // here it outputs '0'
/// here I want word of useState here....
}
return (
<div>
<p>{word}</p>
<p>{underscore}</p>
<button onClick={randomWord}></button>
</div>
)
}
export default App;
The reason why I want to obtain that value inside this function is so I can compare the clicked keybutton (a-z) to the current chosenword. And if there is something wrong with other functions, please feel free to share your feedback here below as well.
You're using a variable defined inside the component render function in a useEffect effect and that variable is missing in the hook's deps. Always include the deps you need (I highly recommend the lint rule react-hooks/exhaustive-deps). When you add checkMatchLetter to deps you'll always have the newest instance of the function inside your effect instead of always using the old version from the first render like you do now.
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, [checkMatchLetter, word]);
This change will make the effect run on every render. To rectify that, you can memoise your callbacks. However, that's a new can of worms.

Resources