I have tried solutions related with Javascript and React using state, but I can't do it with Hooks. I want to add 3 seconds delay for each element that I'm rendering on array.map
import React, { useState } from 'react';
const DelayMapHooks = () => {
const [array, setArray] = useState([1, 2, 3])
return (
<div>
{
array.map((elem, key) => {
// ADD DELAY HERE FOR EACH ELEMENT
return( <div>Number: {elem}</div> )
})
}
</div>
);
};
export default DelayMapHooks;
I have tried adding an await/async immediately after the map, using await delay(3000) with the function
const delay = ms => {return(new Promise(res => setTimeout(res, ms)))}
But it doesn't work, showing the error:
Objects are not valid as a React child (found: [object Promise]). Appreciated any help.
You can't block the render method like that, render should run synchronously. This can be done with useEffect though, you need to modify the state of the array each period. React will then reconcile the DOM and render an extra element every second.
useEffect(() => {
const interval = setInterval(() => {
// You'd want an exit condition here
setArray(arr => [...arr, arr.length + 1]);
}, 1000);
return () => clearInterval(interval);
}, []);
Does this work for you? The delay needs to be handled through state. Either adding the items over time or a visibility property.
import React, { useEffect, useState } from "react";
const DelayMapHooks = () => {
const [array, setArray] = useState([]);
useEffect(() => {
for (let i = 1; i <= 3; i++) {
setTimeout(() => setArray((prevState) => [...prevState, i]), 3000 * i);
}
}, []);
return (
<div>
{array.map((elem, key) => {
return <div>Number: {elem}</div>;
})}
</div>
);
};
export default DelayMapHooks;
You could still use delay, but require some more works
iterate array with delay using IIFE async function
after delay, set the element to be displayed and have a useEffect hook to listen to that update, and then append the to-be-displayed element to a new array using for rendering
const [array] = React.useState([1, 2, 3, 4, 5])
const [displayArray, setDisplayArray] = React.useState([])
const [displayEl, setDisplayEl] = React.useState()
const delay = (ms) =>
new Promise((res) => {
setTimeout(() => {
res()
}, ms)
})
React.useEffect(() => {
(async function () {
for (let el of array) {
await delay(1000)
setDisplayEl(el)
}
setDisplayEl(undefined)
})()
}, [array])
React.useEffect(() => {
displayEl && setDisplayArray((prev) => [...prev, displayEl])
}, [displayEl])
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{displayArray.map((elem, key) => (
<div key={key}>Number: {elem}</div>
))}
</div>
)
Codesandbox for demo (refresh to see from the beginning)
Related
Can someone help me how can I do this in Reactjs?
Can I iterate one IntersectionObserver for multiple child in reactjs
const faders = document.querySelectorAll('.fade-in');
const appearOptions = {
threshold: 1
};
const appearOnScroll = new IntersectionObserver( function(entries, appearOnScroll){
entries.forEach(entry => {
if(!entry.isIntersecting){
return;
}else{
entry.target.classList.add('appear')
appearOnScroll.unobserve(entry.target)
}
})
}, appearOptions);
faders.forEach(fader =>{
appearOnScroll.observe(fader)
})
useIntersectionObserver.js
Use this custom hook. It creates an IntersectionObserver instance and saves it in a useRef hook. It tracks the elements that are being observed in a state. Whenever the state changes, it unobserves all elements and then reobserves the elements that remain in the state.
The advantages of creating a custom hook is that you can reuse the hook and implement it on multiple occasions.
import { useRef, useState, useEffect } from 'react';
const useIntersectionObserver = ({ root = null, rootMargin = '0px', threshold = 0 }) => {
const [entries, setEntries] = useState([]);
const [observedNodes, setObservedNodes] = useState([]);
const observer = useRef(null);
useEffect(() => {
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver(entries => setEntries(entries), {
root,
rootMargin,
threshold
});
const { current: currentObserver } = observer;
for (const node of observedNodes) {
currentObserver.observe(node);
}
return () => currentObserver.disconnect();
}, [observedNodes, root, rootMargin, threshold]);
return [entries, setObservedNodes];
};
export default useIntersectionObserver;
app.js
Use the hook where you need to observe your elements. Create references to the elements that you need to observe and pass them to the hook after the first render.
The entries state will contain an array of IntersectionObserverEntry objects. Loop over it whenever the entries state changes and assert your logic, like adding a class.
import { useRef, useEffect } from 'react';
import useIntersectionObserver from './useIntersectionObserver';
function App() {
const targets = useRef(new Set());
const [entries, setObservedNodes] = useIntersectionObserver({
threshold: 1
});
useEffect(() => {
setObservedNodes(() => ([...targets.current]));
}, [setObservedNodes]);
useEffect(() => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('appear');
setObservedNodes(observedNodes =>
observedNodes.filter(node => node !== entry.target)
);
}
}
}, [entries, setObservedNodes]);
return (
<>
<div className="fade-in" ref={element => targets.current.add(element)}></div>
<div className="fade-in" ref={element => targets.current.add(element)}></div>
</>
)
}
My Goal:
I'm trying to build a component that when you give it props.items and props.fadeEvery, it will act as a text rotator. I eventually want it to fade in an out, but I'm having trouble with my window.setInterval.
Possible Issue:
I'm calling setIndex in the useEffect hook, but is that not good practice? How an I have it iterate through the array items infinitely?
TextFade.tsx
// Imports: Dependencies
import React, { useState, useEffect } from 'react';
// TypeScript Type: Props
interface Props {
items: Array<string>,
fadeEvery: number,
};
// Component: Text Fade
const TextFade: React.FC<Props> = (props): JSX.Element => {
// React Hooks: State
const [ index, setIndex ] = useState<number>(0);
// React Hooks: Lifecycle Methods
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
if (index > props.items.length) {
// Set Data
setIndex(0);
}
else {
// Set Data
setIndex(index + 1);
}
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, []);
return (
<div id="text-fade-container">
<p id="text-fade-text">{props.items[index]}</p>
</div>
);
};
// Exports
export default TextFade;
Your index values are taken from initital closure and it won't update unless useEffect callback is called again. You can instead use functional way to update state
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
setIndex(prevIdx => {
if (prevIdx > props.items.length) {
// Set Data
return 0;
}
else {
// Set Data
return prevIdx + 1;
}
})
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, []);
Below I've knocked up a snippet using the callback version of setState, this avoid the closure issue you get by using useEffect with []..
const {useState, useEffect} = React;
const TextFade = (props) => {
// React Hooks: State
const [ index, setIndex ] = useState(0);
// React Hooks: Lifecycle Methods
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
setIndex(index =>
index + 1 >= props.items.length
? 0
: index + 1);
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearInterval(timeoutID);
}, []);
return (
<div id="text-fade-container">
<p id="text-fade-text">{props.items[index]}</p>
</div>
);
};
ReactDOM.render(<TextFade items={['one','two', 'three']} fadeEvery={1}/>, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
As #Keith said:
index is in what's called a closure. Your use effect has been told to
only render once [].. So you need to use the callback version of
setIndex
So, your useEffect hook will be:
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
if (index > props.items.length) {
// Set Data
setIndex(0);
} else {
// Set Data
setIndex(index + 1);
}
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, [index]);
Here is the working demo at CodeSandbox.
import React, { useState } from "react";
import useInterval from "use-interval";
const useOwnHook = () => {
const arr = [...Array(100)].map((_, index) => index);
return {
arr
};
};
const Component = ({ count }) => {
const { arr } = useOwnHook();
console.log(arr, "arr");
return (
<div className="App">
<h1>{count + 1}</h1>
</div>
);
};
export default function App() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <Component count={count} />;
}
I've created hook useOwnHook for demonstration that each time Component re-render, each time it goes insite useOwnHook, and create new array, is it possible to prevent it to move inside of this hook each time on re-render?
Personally, I'd use a state variable with a init function:
const useOwnHook = () => {
const [arr] = useState(() => [...Array(100)].map((_, index) => index));
return {
arr
};
};
Benefit of the init function is that it's lazy-evaluated so you won't be constructing the array each time the component is rendered.
You can add useState into your custom hook as:
const useOwnHook = () => {
const [arr] = useState([...Array(100)].map((_, index) => index));
return {
arr
};
};
By doing this you can keep the same array in your useOwnHook.
Also you can import as import { useState } from 'react'.
See also from the Using the State Hook documentation - example with a different variable:
We declare a state variable called count, and set it to 0. React will remember its current value between re-renders, and provide the most recent one to our function. If we want to update the current count, we can call setCount.
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;
I have a component and inside of it I am communicating with API (here I put timeout instead for simulation). I make some calls inside for loop to update arrays on current interation indexes. Finnally I want to console.log all arrays with added data. And here is the problem. Loop is going through all iterations and doesn't wait to finish asynchonous tasks so it's printing empty arrays with annotation that there are data but just came now.
I tried to add useState but I didn't solve the problem.
import React, {useEffect} from 'react';
function Ingredients() {
const heatingStepsTime = [];
const heatingStepsTimer = [];
function getTimes(i, type) {
if (type === "time") {
setTimeout(() => {
heatingStepsTime[i] = i;
}, 1000);
} else {
setTimeout(() => {
heatingStepsTimer[i] = i + 1;
}, 1000)
}
}
const getProcess = () => {
for (let i = 0; i < 3; i++) {
getTimes(i, "time");
getTimes(i, "timer");
}
};
useEffect(getProcess, []);
console.log(heatingStepsTime);
console.log(heatingStepsTimer);
return (
<div className="App">
test
</div>
);
}
export default Ingredients;
Is there a way to stop for loop iteration in React so it will continue when asychronous tasks are done?
Try this
function App() {
function getTimes(i) {
return new Promise(resolve => {
setTimeout(() => {
resolve(i);
}, 1000);
});
}
const [heatingStepsTime, setHeatingStepsTime] = useState([]);
const getProcess = () => {
const promises = '0'.repeat(10).split('').map((c, i)=>getTimes(i));
Promise.all(promises)
.then(result=> setHeatingStepsTime(result));
};
useEffect(getProcess, []);
return (
<div className="App">
{heatingStepsTime.map((p,i)=><div key={i}>{p}</div>)}
</div>
);
}
https://stackblitz.com/edit/react-szrhwk
Explanation:
function getTimes(i) {
return new Promise(resolve => {
setTimeout(() => {
resolve(i);
}, 1000);
});
}
To simulate the API, you should simulate it with a promise. This timer will resolve the promise when it times out.
const [heatingStepsTime, setHeatingStepsTime] = useState([]);
You want to store your result into a state so that react knows to render the result
'0'.repeat(10).split('')
This is just to simulate your for loop... you may ignore this if you have multiple API calls... just replace it with
const promises = [apiCall1(), apiCall2()];
Promise.all(promises)
.then(result=> setHeatingStepsTime(result));
this will wait for all the promises to resolve and store the result into the state
I found a solution. In React we need to use useState to update arrays properly through all iterations. Here is fixed code - maybe someone will have similar problem so here is solution:
import React, {useState, useEffect} from 'react';
function Ingredients() {
const [heatingStepsTime, setTime] = useState([]);
const [heatingStepsTimer, setTimer] = useState([]);
function getTimes(i, type) {
if (type === "time") {
setTimeout(() => {
setTime(currentTime => currentTime.concat(i))
},1000);
} else {
setTimeout(() => {
setTimer(currentTime => currentTime.concat(i))
}, 1000)
}
};
const getProcess = () => {
for (let i = 0; i < 3; i++) {
getTimes(i, "time");
getTimes(i, "timer");
}
}
useEffect(getProcess, []);
console.log(heatingStepsTime);
console.log(heatingStepsTimer);
if (heatingStepsTime[2] && heatingStepsTimer[2] && heatingStepsTime[2] === heatingStepsTimer[2]) {
console.log("Works!", heatingStepsTime[1], heatingStepsTimer[1])
}
return (
<div className="App">
test
</div>
);
}
export default Ingredients;