React hooks, update one state in response of another state update - reactjs

How to have second state produced from first state and when the first one changes, make second state to react in response to update it ?
It worked but I am not sure how reliable is doing it that way, any suggestions ? Thanks.
export default function App() {
const [arr, setArr] = useState([1, 2, 3, 4, 5]);
const [length, setLength] = useState(arr.length);
console.log(arr)
const clickHandler = () => {
setArr([...arr, arr.length+1])
};
useEffect(() => {
setLength(length + 1)
}, [arr])
return (
<div className="App">
<h2>{length}</h2>
<button onClick={clickHandler}>click</button>
</div>
);
}

so you want the 'second state' (length) to be updated every time the length of the array changes. For that, you need an effect that will take the length of the array as a dependency (arr.length), and then set length to that value.
Also added a change to the clickHandler function as the state is computed from the previous state (check this for further details)
This is how the final code would look
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([1, 2, 3, 4, 5]);
const [length, setLength] = useState(arr.length);
console.log(arr);
const clickHandler = () => {
setArr((arr) => [...arr, arr.length + 1]); // Change #1
};
useEffect(() => {
setLength(arr.length); // Change #2
}, [arr.length]);
return (
<div className="App">
<h2>{length}</h2>
<button onClick={clickHandler}>click</button>
</div>
);
}

Related

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;

How to set order of functions execution inside useEffect hook

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.

Can't push from useEffect a new item in array

I have the next situation in my react application:
export default function App() {
const arr = []
useEffect(() => {
arr.push('test')
},[])
console.log(arr)
return (
<div className="App">
{
arr.map((i) => <span>{i}</span>)
}
</div>
);
}
Above i try to push a new item in the array. So in the console.log(arr) i expect to have ['test']. Why it does not happen? demo: https://codesandbox.io/s/reverent-mayer-pxswq?file=/src/App.js:59-294
useEffect runs in the next frame, so it won't be populated until later.
You should try this:
const arr = []
useEffect(() => {
arr.push('test')
console.log(arr)
},[])
You also don't want to be setting state like that, use useState.
const [arr, setArr] = useState([])
useEffect(() => {
setArr(a => [...a, 'test'])
}, [arr])
You should use the state hook for your arr.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
setArr([...arr, "test"]);
}, []);
console.log(arr);
return (
<div className="App">
{arr.map((i) => (
<span>{i}</span>
))}
</div>
);
}
The code you shared is working but the console.log is not at the good place.
Your console.log is login before the useEffect();
export default function App() {
const arr = []
useEffect(() => {
arr.push('test');
console.log('After UseEffect', arr);
},[])
console.log('Before UseEffect',arr)
return (
<div className="App">
{
arr.map((i) => <span>{i}</span>)
}
</div>
);
}
In your case, useEffect run after console.log so arr still empty when log,
Agree with #Stoobish, but:
Tracking arr change should use useDebugValue or log inside useEffect
If you want to prevent duplicate render when state change, use useEffectLayout instead of useEffect.
import { useEffect, useLayoutEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useLayoutEffect(() => {
setArr([...arr, "test"]);
}, []);
useEffect(() => {
console.log(arr);
}, [arr])
return (
<div className="App">
{arr.map((i) => (
<span key={i}>{i}</span>
))}
</div>
);
}
You should consider three point below:
You should use useState hook.
So Use
const [arr, setArr] = useState([]);
Not
const arr = []
When you use React you must consider useState because when you update any variable using useState Hook then React will update the state and can be able to rerender your component. Otherwise, your component will not be updated.
You should not push directly into the array arr.
So Use
setArr(item => [...item, newItem])
Not
arr.push('test')
As I explain it above, if you dont use setArr then React will not update your component.
You should use key when you use multiple html element using map function.
So Use
{arr.map((a, i) => (
<span key={i}>{a}</span>
))}
Not
arr.map((i) => <span>{i}</span>)
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
Here is the complete example:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
let newItem = 'test';
setArr(item => [...item, newItem]); // Here we are using `spread operator` to use previous items from `arr`
console.log(arr);
}, []);
return (
<div className="App">
{arr.map((a, i) => (
<span key={i}>{a}</span>
))}
</div>
);
}

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>;
};

Counter increment on setInterval

import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}

Resources