React changing order of components does not trigger a re-render - javascript

I'm designing a system that shows when my Heroku apps have been updated. The problem I had was the parent component (Herokus) decided the order, but I wanted them to sort based on when they had been updated starting with the most recent. I was able to achieve this, by passing the dates from the Heroku component back to the parent Herokus component. I tried adding a console.log(this.state.apps) which shows the sorted order, but that is not reflected by the rendered component. My guess is react is not seeing the changed order of the components as a reason to update the view - but to be honest I have no clue. Any help is appreciated. Files are below. Sorry, my code is a mess, I rewrote it three times trying to fix this issue.
Herokus Component:
import React from 'react';
import Heroku from './Heroku';
export default class Herokus extends React.Component {
constructor(props) {
super(props);
var apps = [
'example-heroku-app'
]
this.state = {
apps: apps.map((h) => {
return {
app: h,
updatedAt: new Date()
};
})
}
}
sortApps() {
var sorted = this.state.apps.sort((a, b) => {
return a.updatedAt < b.updatedAt;
});
this.setState({
apps: sorted
});
}
updateParrent(data){
var apps = this.state.apps;
apps.find(x => x.app === data.app).updatedAt = data.updatedAt;
this.setState({
apps: apps
});
this.sortApps();
this.forceUpdate();
}
render() {
var s = this;
return (
<div>
{s.state.apps.map((app, i) => {
return (
<Heroku app={app.app} compact key={`heroku-${i}`} updateParrent={s.updateParrent.bind(s)}/>
);
})}
</div>
);
}
}
Heroku Component:
import React from 'react';
export default class Ping extends React.Component {
constructor(props) {
super(props);
this.state = {
app: props.app,
updatedAt: new Date()
};
}
componentDidMount() {
var s = this;
axios.get(`/api/heroku/app/${s.props.app}`, {
timeout: 20000,
})
.then(res => {
var data = res.data;
s.setState({
app: data.app.name,
updatedAt: new Date(data.app['updated_at'])
});
s.props.updateParrent(s.state);
setInterval(() => {
var updatedAt = new Date(s.state.updatedAt);
s.setState({
updatedAt: updatedAt
});
}, 1000);
});
}
howLongAgo(date) {
var ms = new Date() - date;
var seconds = ms / 1000;
var n = seconds;
var suffix = '';
// less than 1 minute
if(seconds < 60){
n = seconds;
suffix = 'second' + (n > 1.5 ? 's' : '');
}
// less than 1 hour
else if(seconds < 3600) {
n = seconds / 60
suffix = 'minute' + (n > 1.5 ? 's' : '');
}
// less than 1 day
else if(seconds < 86400) {
n = seconds / 3600;
suffix = 'hour' + (n > 1.5 ? 's' : '');
}
// less than 1 week
else if(seconds < 604800) {
n = seconds / 86400;
suffix = 'day' + (n > 1.5 ? 's' : '');
}
// more than 1 week
else {
n = seconds / 604800;
suffix = 'week' + (n > 1.5 ? 's' : '');
}
return `${Math.round(n)} ${suffix} ago`;
}
render() {
var s = this.state;
var self = this;
var output;
// COMPACT VIEW
if(this.props.compact){
output = (
<div className={'heroku heroku-compact'}>
<h3>{s.app}</h3>
<p>{self.howLongAgo(s.updatedAt)}</p>
<div className='clearfix'></div>
</div>
)
// FULL SIZE VIEW
} else{
output = ()
}
return output;
}
}

The problem seems to be that the order changes, but since the keys are based off the i index variable, the recorded list still has the same keys in the same order. React therefore does not see a difference.
Example:
Unsorted apps a, b, and c
<div key="1"> //app b
<div key="2"> //app a
<div key="3"> //app c
Sorted apps
<div key="1"> //app a
<div key="2"> //app b
<div key="3"> //app c
The apps are now in the right order, but the parent component Herokus still seems them with keys 1, 2, and 3 in the same order.
The solution:
Change the key to something unique to the individual Heroku components, but something that will remain unchanged unlike updatedAt. In this case the most logical choice is the app name.
This can be implemented in the Herokus component's render function like so:
export default class Herokus extends React.Component {
render() {
var s = this;
return (
<div>
{s.state.apps.map((app, i) => {
return (
<Heroku app={app.app} compact key={`heroku-${app.app}`} updateParrent={s.updateParrent.bind(s)}/>
);
})}
</div>
);
}
}

Related

Create a function that returns the next/prev 4 results in an array of objects

I'm trying to create a function that renders the next & prev 4 results in an array of objects onClick. Currently I am only returning the first 4 items and their images and adjusting the values in the return statement onClick and am not happy with this solution. Any pointers?
const ImageSlider = ({ loadData, data, setData }) => {
const [current, setCurrent] = useState(0);
const length = data.length;
const [test, setTest] = useState(0);
const [another, setAnother] = useState(4);
useEffect(() => {
loadData().then((data) => setData(data));
}, []);
const getNextSlide = () => {
setCurrent(current === length - 1 ? 0 : current + 1);
if (current / 3 === 1) {
setTest(test + 4);
setAnother(another + 4);
}
};
const getPrevSlide = () => {
setCurrent(current === 0 ? length - 1 : current - 1);
// setTest(test - 1);
// setAnother(another - 1);
};
console.log(current);
if (!Array.isArray(data) || length <= 0) {
return null;
// Need that error message here
}
return (
<div className="slider">
<FaArrowCircleLeft className="slider-left-arrow" onClick={getPrevSlide} />
<FaArrowCircleRight
className="slider-right-arrow"
onClick={getNextSlide}
/>
{data.slice(test, another).map((program, index) => {
return (
<div
className={
index === current
? "slider-active-program"
: "slider-inactive-program"
}
key={index}
>
<img
src={program.image}
alt="program"
className="slider-program-image"
/>
</div>
);
})}
</div>
);
};
export default ImageSlider;
This may be one possible approach to achieve the desired objective:
const ImageSlider = ({ loadData, data, setData }) => {
const imageLength = 4; // number of images to be shown per-page
const totalImages = data.length;
const [startAt, setStartAt] = useSate(0); // start rendering from image-0
useEffect(() => {
loadData().then((data) => setData(data));
}, []);
const switchImages = (direction = 'forward') => (
setStartAt(prev => {
if (direction === 'forward') {
// if there are more images, reposition 'startAt' to next set
if (prev + imageLength < totalImages) return prev + imageLength;
return 0; // reset back to image-0
} else {
// if there is previous set, reposition to it's start
if (prev - imageLength >= 0) return prev - imageLength;
// otherwise, reposition to the start of the last-set
return (Math.floor((totalImages - 1) / imageLength) * imageLength)
};
})
);
return (
<div className="slider">
<FaArrowCircleLeft
className="slider-left-arrow"
onClick={() => switchImages('reverse')}
/>
<FaArrowCircleRight
className="slider-right-arrow"
onClick={() => switchImages()}
/>
{Array.isArray(data) && data.filter((el, idx) => idx >= startAt && idx < (startAt + imageLength)).map((program, index) => {
return (
<div
className={
index === current
? "slider-active-program"
: "slider-inactive-program"
}
key={index}
>
<img
src={program.image}
alt="program"
className="slider-program-image"
/>
</div>
);
})}
</div>
);
};
export default ImageSlider;
Explanation
imageLength is fixed (set to 4)
startAt is a state-variable (initially set to 0)
data is rendered on return only when it is an Array
switchImages is a method invoked on both left-arrow and right-arrow click-events (changing the parameter direction to reverse on left-arrow)
startAt is updated based on the direction, the prev value, the totalImages and imageLength.
Suggestion
If one needs to render a message to the user that the data array is either not-yet-loaded or it is empty, it may be acccomplished by changing the {Array.isArray(data) && .... into a conditional such as {Array.isArray(data) ? ....<existing> : <some message here>.
Warning
The above code is only an approach. It is not tested & may contain syntax errors or other issues. Please use comments to share feedback and this answer may be updated accordingly.
I don't think you have to change much about your approach to get what you're looking for here.
To render the images to the left and right of your current selection and allow looping from the front to the back (as it looks like you want to do), we can paste a copy of the array to the front and back and select the middle slice. It's a little bit like this:
The implementation would just require you to replace the current .map(...) method in your return statement with this one:
[...data, ...data, ...data].slice(
length + current - peekLeft,
length + current + peekRight + 1
).map((program, index) => {
...
}
And then you can also remove the test and another state objects as well since they aren't necessary. You just need to add the peekLeft and peekRight or replace them inline inside the .map(...) statement. The first option may look something like this:
const peekLeft = 4;
const peekRight = 4;
const ImageSlider = ({ loadData, data, setData }) => {
...
}

Is there a way to iterate through a timer in React?

I'm trying to create a Pomodoro timer using Hooks and I have set up the basic functionality using useState and useEffect. I have a 25-minute timer that counts down and every time it gets to 0, it starts a break timer of 5 minutes. What I'm trying to figure out now is how to create an iteration that says "every 4 times the timer hits 0, change the break time from 5 minutes to 15 minutes and then, go back to 5 minutes." I thought of creating sessions that way it will say 4th session and then it will go back to 1. but I'm really not sure what to do here.
import React, { useState, useEffect } from "react";
function Pomodoro() {
const [minutes, setMinutes] = useState(25);
const [seconds, setSeconds] = useState(0);
const [displayMessage, setDisplayMessage] = useState(false);
const [session, setSession] = useState(1);
useEffect(() => {
let interval = setInterval(() => {
clearInterval(interval);
if (seconds === 0 && minutes !== 0) {
setSeconds(59);
setMinutes(minutes -1);
} else if (seconds === 0 && minutes === 0) {
let minutes = displayMessage ? 24 : 4;
let seconds = 59;
setSeconds(seconds);
setMinutes(minutes);
setDisplayMessage(!displayMessage);
} else {
setSeconds(seconds -1);
}
}, 1000);
}, [seconds]);
const timerMinutes = minutes < 10 ? `0${minutes}` : minutes;
const timerSeconds = seconds < 10 ? `0${seconds}` : seconds;
return (
<div className="pomodoro">
<div>Session:{session} </div>
<div className="message">
{displayMessage && <div>Break time! New Session starts in:</div>}
</div>
<div className="timer">
{timerMinutes}:{timerSeconds}
</div>
</div>
);
}
export default Pomodoro;
Your approach using a counter to keep track of the completed sessions seems to make sense. If you want to use a different amount of break time for every fourth iteration, you could use the remainder operator as below:
let breakTime = (session % 4) === 0 ? 14 : 0;
Then, you just need to make sure you are incrementing your session variable by one each time you complete a session. This also means you only want to increase it when you are not "on break" so you must make sure to guard against that.
Updating the answer with the full code that I tested to be working. Note the following changes I made:
I am only keeping track of the timer in seconds - this reduces the complexity inside useEffect and you can convert from seconds to other formats (try using the remainder operator again)
Moved the period lengths to constants
Renamed the variable displayMessage to isOnBreak for clarity
import React, { useState, useEffect } from "react";
// Define the period lengths (in seconds)
const workTime = 2;
const shortBreakTime = 4;
const longBreakTime = 6;
function Pomodoro() {
const [seconds, setSeconds] = useState(workTime);
// Renamed this variable for clarity to indicate it is a boolean
const [isOnBreak, setIsOnBreak] = useState(false);
const [session, setSession] = useState(1);
useEffect(() => {
let interval = setInterval(() => {
clearInterval(interval);
if (seconds === 0) {
let breakTime = (session % 4 === 0) ? longBreakTime : shortBreakTime;
let seconds = !isOnBreak ? breakTime : workTime;
// A session is complete when work and break is done,
// so only increment when finishing a break
if (isOnBreak) setSession(session+1);
setSeconds(seconds);
setIsOnBreak(!isOnBreak);
} else {
setSeconds(seconds -1);
}
}, 1000);
}, [seconds]);
// Here you could convert from seconds to minutes and seconds or whatever time format you prefer
const timerSeconds = seconds < 10 ? `0${seconds}` : seconds;
return (
<div className="pomodoro">
<div>Session:{session} </div>
<div className="message">
{isOnBreak && <div>Break time! New Session starts in:</div>}
</div>
<div className="timer">
{timerSeconds}
</div>
</div>
);
}

Building a React clock with hooks

I'm working on building a clock that counts up, just a practice exercise (I know there are better ways of doing this).
My issue is when a minute is added, the "addMinute" seems to run twice, and I can't for the life of me figure out how or why.
Here is a demo on codesandbox: https://codesandbox.io/s/restless-frost-bud7p
And here is the code at a glance:
(please note, the counter only counts up to 3 and counts faster; this is just for testing purposes)
const Clock = (props) => {
const [seconds, setSeconds] = useState(0)
const [minutes, setMinutes] = useState(0)
const [hours, setHours] = useState(0)
const addHour = () => {
setHours(p => p + 1)
}
const addMinute = () => {
setMinutes(prev => {
if (prev === 3) {
addHour()
return 0
} else {
return prev + 1
}
})
}
const addSecond = () => {
setSeconds(prevState => {
if (prevState === 3) {
addMinute()
return 0
} else {
return prevState + 1
}
})
}
useEffect(() => {
const timer = setInterval(addSecond, 600)
return () => clearInterval(timer)
}, [])
return (
<div>
<h1>time!</h1>
<p>
{hours < 10 ? 0 : ""}{hours}
:
{minutes < 10 ? 0 : ""}{minutes}
:
{seconds < 10 ? 0 : ""}{seconds}
</p>
<p>
{seconds.toString()}
</p>
</div>
)
}
The issue is that you are using the React.StrictMode wrapper in the index.js file.
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
So you should decide between using strict mode or having side effects, the easy way is just removing the React.StrictMode wrapper. The other way is removing side effects, where you only need to do the following:
Update your addSecond and addMinute functions to something like:
const addMinute = () => {
setMinutes((prev) => prev + 1);
};
const addSecond = () => {
setSeconds((prevState) => prevState + 1);
};
And your useEffect call to something like:
useEffect(() => {
if(seconds === 3) {
addMinute();
setSeconds(0);
};
if(minutes === 3) {
addHour();
setMinutes(0);
}
const timer = setInterval(addSecond, 600);
return () => clearInterval(timer);
}, [seconds, minutes]);
Here an updated version of your code: https://codesandbox.io/s/goofy-lake-1i9xf
A couple of issues,
first you need to use prev for minutes, so
const addMinute = () => {
setMinutes(prev => {
if (prev === 3) {
addHour()
return 0
} else {
return prev + 1
}
})
}
And then you need to remove the React.StrictMode wrapper component from index, which is what is actually causing the double increase, as part of what the strict mode does is
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
see: https://codesandbox.io/s/pensive-wildflower-pujmk
So I had no idea about strict mode and the intentional double renders. After reading the documentation I finally understand the purpose of this.
As such, it appears the best solution is to have no side effects from the useEffect, and instead, handle that logic outside of the effect, but still changing every second.
So, I set an effect that has a piece of state starting at zero and going up by one per second.
Then, with each change of "time", the useMemo will recalculate how many hours, mins and seconds the total time is.
The only thing I don't like is all those calculations running every render! (But realistically those take but a few miliseconds, so performance doesn't seem to be an issue).
const [time, setTime] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setTime(p => p + 1)
}, 999);
return () => clearTimeout(timer);
}, [userState]);
const timeJSON = useMemo(() => {
const hrs = Math.floor(time/3600)
const mins = Math.floor( (time-(3600*hrs)) / 60 )
const secs = time - (3600 * hrs) - (60*mins)
return {
hrs,
mins,
secs,
}
}, [time])
return (
<div>
<p>
{timeJSON.hrs < 10 ? 0 : ""}{timeJSON.hrs}
:
{timeJSON.mins < 10 ? 0 : ""}{timeJSON.mins}
:
{timeJSON.secs < 10 ? 0 : ""}{timeJSON.secs}
</p>
</div>
)
Thanks again for everyone pointing me in the right direction on this!

Problem updating a state using setInterval in React application

I have a function called getRandomHexagram() which returns a random 6-character string that is used for props in my <hexagram string={stringstate} /> component in my I Ching divination app.
I also have a function called resettimer() which retrieves this random 6-character string multiple times and passes it to my component within a few seconds, so that while setInterval is running, the component will keep changing its appearance until clearInterval is called.
I would like to continuously update the state using resettimer() function run until it ends, and then use the new state in another function together with clearInterval.
What is strange to me is that while the if portion of the timer is running, the component continuously updates as it should. But when I try to call the final state in the } else { statement together with clearInterval, console always shows that stringstate is still "VVVVVV", or whatever was the last prop that my component used in my previous click, NOT what my component is currently using.
function getRandomHexagram() {
const flipcoins = () => {
return (
(Math.floor(Math.random() * 2) + 2) +
(Math.floor(Math.random() * 2) + 2) +
(Math.floor(Math.random() * 2) + 2)
);
};
const drawline = (coinvalue) => {
let line;
if (coinvalue == 6) {line = "B";}
if (coinvalue == 7) {line = "V";}
if (coinvalue == 8) {line = "P";}
if (coinvalue == 9) {line = "W";}
return line;
};
return (
drawline(flipcoins()) +
drawline(flipcoins()) +
drawline(flipcoins()) +
drawline(flipcoins()) +
drawline(flipcoins()) +
drawline(flipcoins())
);
}
function App() {
const [stringstate, setStringstate] = useState("VVVVVV");
function resettimer() {
var timesrun = 0;
var randomtime = Math.floor(Math.random() * 15) + 10;
const interval = setInterval(() => {
timesrun += 1;
if (timesrun < randomtime) {
thisString = getRandomHexagram();
setStringstate(thisString);
console.log("the current permutation is: " + thisString);
// console and component shows this correctly.
} else {
clearInterval(interval);
console.log("the final state state is: " + stringstate);
// Call another function here using the NEW stringstate
// But console shows that stringstate is still showing
// the old state of VVVVVV despite my component showing otherwise
}
}, 100);
}
... ...
return (
<div className="App">
<Button onClick={() => resettimer()}>
<Hexagram string={stringstate} />
</div>
);
}
When the button is clicked and the function invoked, my console shows this:
the current permutation is: VPPVPB
the current permutation is: VPPPVV
the current permutation is: BPBVBV
the current permutation is: VVPVPV
the current permutation is: PPBVPV
the current permutation is: PPPBBB
the current permutation is: VVPPPV
the final state state is: VVVVVV
Any idea why this is happening? Any advice is much appreciated.

How to create an arbitrary mapping in javascript?

I have a React app where I want to display lunar phases. To that end I tried to create an empty array and then map it appropriately:
import React from 'react'
export const MoonPhase = (props) => {
let start = new Date(props.start)
let end = new Date(props.end)
let delta = (end.getTime() - start.getTime()) / 1000
let quarters = new Array(Math.floor(
// Each lunar month is 2.55ᴇ6s.
(delta / (2.55 * Math.pow(10, 6))) * 4
))
return (
<div className='phases'>
{quarters.map((_, i) => {
switch(i % 4) {
case 0: return <span>πŸŒ‘</span>
case 1: return <span>πŸŒ“</span>
case 2: return <span>πŸŒ•</span>
case 3: return <span>πŸŒ—</span>
}
})}
</div>
)
}
This doesn't produce anything, but if I change quarters to [1,2,3,4,5] I get output.
How should I approach this?

Categories