Local variable value inside measure - javascript

I have a View with nested items, implementing list-like block displaying received props array and I want to get height of this block from sum of height of all of its children. So my component looks like this:
class MultiColumnBox extends Component {
state = {
calculatedMaxHeight: null,
};
measureBlock = () => {
let totalHeight = 0;
//this.props.data is array
this.props.data.forEach((item, index) => {
this['checkbox_${index}'].measure((x , y, width, height) => {
console.log(height)
totalHeight += height;
})
if (index === (this.props.data.length - 1)) {
this.setState({
calculatedMaxHeight: totalHeight
})
}
})
};
render() {
return (
<View
onLayout={() => this.measureBlock()}
>
{this.props.data.map((item, index) => (
<View ref={node => this[`checkbox_${index}`] = node}>
// some content....
</View>
)}
</View>
)
}
}
But if there will be for example 5 blocks of height 40dp, it will console log 40 five times and set state.calculatedMaxHeight to 0. But if I move if (index === (this.props.data.length - 1)) condition inside measure function, it will work okay and set state.calculatedMaxHeight to 200. Why is it works like that? Regardless of nesting inside measure function I work with the same index variable and increase same totalHeight varable, defined once outside the loop.

I think the issue is that setState function is called before the callback function inside measure invoked for first node.
console.log(height) would be printing the 40 for 5 times, But the calculatedMaxHeightt is set to 0 before first print of height value.

setState forces the component to re-render... It is not good practice to call setState within a render (which is what you are doing) as it will cause an infinite render loop (which I believe is the issue you are running into).
I would say pull the calculation out of the state. Put it on the this level and use it however you plan from there.

Related

setTimeout Function Not Removing Characters Like A Queue

This is a simple for loop that runs 80 times but only every 100 ms. Each time it runs, it pushes a Y coordinate for a character into the stillChars state array. The stillChars state array is mapped through to create a sequence of text elements directly underneath eachother.
See this video to see the animation
const StillChars = () => {
const [stillChars, setStillChars] = useState([]);
function addChar() {
for (let i = 0; i < 80; i++) {
setTimeout(() => {
setStillChars((pS) => [
...pS,
{
y: 4 - 0.1 * 1 * i,
death: setTimeout(() => {
setStillChars((pS) => pS.filter((c, idx) => idx !== i));
}, 2000),
},
]);
}, i * 100);
}
}
useEffect(() => {
addChar();
}, []);
return (
<>
{stillChars.map((c, i) => {
return <StillChar key={i} position={[0, c.y, 0]} i={i} />;
})}
</>
);
};
Code Sandbox
Desired behavior: The death: setTimeout function should remove the characters like a queue instead of whatever is going on in the video/codesandbox.
Your main mistake is with:
setStillChars((pS) => pS.filter((c, idx) => idx !== i))
The problem is that this sits within a setTimeout() so the above line runs at different times for each iteration that your for loop does. When it runs the first time, you update your state to remove an item, which ends up causing the elements in your array that were positioned after the removed item to shift back an index. Eventually, you'll be trying to remove values for i that no longer exist in your state array because they've all shifted back to lower indexes. One fix is to instead associate the index with the object itself by creating an id. This way, you're no longer relying on the position to work out which object to remove, but instead, are using the object's id, which won't change when you filter out items from your state:
{
id: i, // add an `id`
y: 4 - 0.1 * 1 * i,
death: setTimeout(() => {
setStillChars((pS) => pS.filter(c => c.id !== i)); // remove the item based on the `id`
}, 2000),
},
Along with this change, you should now change the key within your .map() to use the object's id and not its index, otherwise, the character won't update in your UI if you're using the same index to represent different objects:
return <StillChar key={c.id} position={[0, c.y, 0]} i={i} />;
As also highlighted by #KcH in the question comments, you should also remove your timeouts when your component unmounts. You can do this by returning a cleanup function from your useEffect() that calls clearTimeout() for each timeout that you create. In order to reference each timeout, you would need to store this in an array somewhere, which you can do by creating a ref with useRef(). You may also consider looking into using setInterval() instead of a for loop.

Is there a way to pass/iterate an array as a style attribute in React?

I have an array of randomly generated Hex colors with 8 objects and i'm trying to get each hex code from the array and pass it into the backgroundColor style of a component in React
let colorsArr = [];
useEffect(() => {
for (let i = 0; i <= 8; i++) {
const generateColor =
"#" + Math.floor(Math.random() * 16777215).toString(16);
colorsArr.push(generateColor);
}
}, []);
This returns an array like this with different values each time the page is loaded
["#4c9cae", "#db8e9b", "#1f78aa", "#c4c1d9", "#38b1c5", "#2a3833", "#97da7a", "#543e93", "#6cc0d4"]
My attempt at it was just indexing each value at each component after the for loop ends but it doesn't work and i have no actual clue and uppon researching i didnt find anything
const colorCheckHandler = () => {
// check if for loop has ended and array is complete
if (colorsArr.length >= 8) {
return true;
} else {return false;}
}
// render component
{colorCheckHandler ?
<span style = {{
backgroundColor: `${colorsArr[0]}`
}}
/>
: null}
useEffect() hooks run after the page has mounted or rendered so your "colorArr" might be empty on the initial page load. You can provide an initial value to the "colorArr" and then change it later when the for loop in useEffect() executes after the page has loaded.
You should call your function. like this colorCheckHandler()
// render component
{colorCheckHandler() ?
<span style = {{
backgroundColor: `${colorsArr[0]}`
}}
/>
: null}

React - weird behavior with '.unshift()' but not with '.push()' on state array

I'm trying to make a bidirectional scroll component in React that basically renders chunks of weeks based on the scroll position so if user reaches top past data is rendered and if bottom - future data is rendered.
Here's some very basic code.
const [chunks, setChunks] = useState(initChunks);
const handleReachEdge() => {
const _chunks = chunks.slice();
const lastChunk = conditionOnReachTop ? _chunks[0] : _chunks[_chunks.length - 1]
const newChunk = {
position: conditionOnReachTop ? lastChunk.position - 1 : lastChunk.position + 1;
//position is basically my component key
...data
}
conditionOnReachTop ? _chunks.unshift(newChunk) : _chunks.push(newChunk);
setChunks(_chunks);
}
const renderChunks = () => {
return chunks.map(chunk => <SomeComponent key={chunk.position} />
}
The position parameters are [..., -2, -1, 0, 1, 2, ...]
My problem is that when data is pushed to the array and then state is set everything works fine - the new component renders and the rest doesn't re-render. When the array is unshifted and then the state is set all components mapped from that array are re-rendered, even though I provided unique keys for all of them. They're even React.memo components.
The 'SomeComponent' makes an api call on render so the fact that they all rerender is kinda important.
What am I doing wrong and how can I fix it?

change state with conditionals inside a UseEffect (simple counter with hooks)

I'm trying to build a simple percentage counter from 0 to 100 that is updating itself using SetInterval() inside useEffect(). I can get the counter work but I would like to restart the counter once it reaches the 100%. This is my code:
const [percentage, setPercentage]=useState(0);
useEffect(() => {
const intervalId= setInterval(() => {
let per = percentage=> percentage+1
if(per>=100){
per=0
}
setPercentage(per)
}, 100);
return () => {
}
}, [])
Inside the console I can see the state is increasing but it will ignore the if statement to reset the state to 0 once it reaches 100. How can I tackle this knowing that if conditionals are not great with hooks setState?
Check the percentage instead of per. per is of type function and will never be greater or equal to 100, percentage is the value that will reach 100.
This will make your effect depend on percentage which you have avoided by using the function. In this situation, if I still don't want to add that dependency, then I might use a reducer instead to manage that state. This way I don't need to depend on percentage inside of the useEffect.
const reducer = (state, action) => state >= 100 ? 0 : state + 1;
The way you would do this while keeping useState is by moving the check into the state setting function.
setPercentage(percentage => percentage >= 100 ? 0 : percentage + 1);
This might be the quicker option for you. Notice how similar these are, in the end useState is implemented using the useReducer code path as far as I know.
Below should work.
const [percentage, setPercentage] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setPercentage(prev => prev >= 100 ? 0 : prev + 1);
}, 100);
return () => clearInterval(intervalId);
}, [])
Note that per in your code is a function and therefore cannot used to compare against numbers. You may also want to clear the interval in destruction of component.

How to use setState to splice into an array in the state?

My state.events is in array that is made up of the component instance: EventContainer.
I want my setState to place a new EventContainer in the state.events array. However, I want that EventContainer to go in the index immediately after the specific EventContainer that made the setState call.
I'm looking for help with making the adjustments necessary to my approach or, if my entire approach is bad, a recommendation on how to go about this. Thank you very much.
I'm developing an itinerary builder which is made up of rows/EventContainers that represent an activity on a given day.
Each EventContainer has a button that needs to offer the user the ability to onClick an additional row immediately after that EventContainer.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState (index){
let newEvent = <EventContainer />;
this.setState(prevState => {
const events = prevState.events.map((item, j) => {
if (j === index) {
events: [...prevState.events.splice(index, 0, newEvent)]
}
})
})
}
render(){
return (
<>
<div>
<ul>
{
this.state.events === null
? <EventContainer pushNewEventContainerToState= .
{this.pushNewEventContainerToState} />
: <NewEventButton pushNewEventContainerToState={this.pushNewEventContainerToState} />
}
{this.state.events.map((item, index) => (
<li
key={item}
onClick={() =>
this.pushNewEventContainerToState(index)}
>{item}</li>
))}
</ul>
</div>
</>
)
}
}
My goal in setState was to splice newEvent into this.state.events immediately after the index (the parameter in pushNewEventContainerToState function).
I'm getting this error but I'm guessing there's more going on than just this: Line 23:22: Expected an assignment or function call and instead saw an expression no-unused-expressions.
I can see at least 2 issues with the code.
- Splice will mutate the array in place
- You are not returning the updated state.
You can instead use slice to build the new array.
pushNewEventContainerToState(index) {
let newEvent = < EventContainer / > ;
this.setState(prevState => {
const updatedEvents = [...prevState.events.slice(0, index], newEvent, ...prevState.events.slice(index + 1];
return {
events: updatedEvents
})
})
}
As I'm fairly new to coding, it took me awhile but I was able to compile the full answer. Here is the code, below. Below that, I explain, point by point, what the problem was and how the updated code addresses that.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [{key:0}],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState(index) {
let newEvent = {key: this.state.events.length};
this.setState(prevState => {
let updatedEvents = [...prevState.events.slice(0, index + 1), newEvent, ...prevState.events.slice(index + 1)];
return {
events: updatedEvents
};
})
}
render(){
return (
<>
<div>
<ul>
{this.state.events.map((item, index) => (
<li key={item.key}>
< EventContainer pushNewEventContainerToState={() => this.pushNewEventContainerToState(index) } / >
</li>
))}
</ul>
</div>
</>
)
}
}
Setup
Starting with state.events, instead of starting with an empty array, I'm starting with one object, including a key starting at 0, because I always want the user to start with one EventContainer.
Regarding pushNewEventContainerToState, #Sushanth made a great recommendation. Please refer directly to that function in my latest code. The refinement I made has to do with the way I separate the EventContainer being passed to this.state.events. I've moved the EventContainer from pushNewEventContainerToState down to the render() element. I've given it a prop of key={item.key} and wrapped the component instance in a li. The very first EventContainer will have a key of 0 (see state.events[0]). Now, each new EventContainer passed to state.events will have a key that's based off the latest .length() of the state.events array (refer to the latest value of the let newEvent variable in pushNewEventContainerToState).
All of that allowed me to fix a big problem I was facing: I needed the newest EventContainer to be placed in the index immediately after the index of the EventContainer calling pushNewEventContainerToState. The main reason this was happening was because I wasn't properly passing the index to the EventContainer inside of render(). Now that I have the actual EventContainer there, I can pass it a prop in the right manner (please refer EventContainer's prop in render). Now I'm calling pushNewEventContainerToState with the correct index.

Categories