Reputation: 2947
I am following this tutorial on making a javascript calendar and trying to implement it in react
The working javascript version is in this jsfiddle
import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')
const Home: NextPage = () => {
const today = new Date()
const [currentMonth, setCurrentMonth] = useState(today.getMonth())
const [currentYear, setCurrentYear] = useState(today.getFullYear())
const calendarBodyRef = useRef<HTMLDivElement>(null)
const rows = 6
const cells = 7
const firstDay = (new Date(currentYear, currentMonth)).getDay()
// check how many days in a month code from https://dzone.com/articles/determining-number-days-month
const daysInMonth = (iMonth: number, iYear: number) => {
return 32 - new Date(iYear, iMonth, 32).getDate()
}
return (
<>
<Head>
<title>Calendar Budget App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.calendarWrap}>
<h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
<div className={styles.daysWrap}>
<span>Sun</span>
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
</div>
<div ref={calendarBodyRef} className={styles.calendarBody}>
{[...Array(rows).keys()].map((row) => {
let date = 1
return (
<div key={row} className={styles.row}>
{[...Array(cells).keys()].map((cell) => {
if (row === 0 && cell < firstDay) {
return (
<div key={cell} className={styles.cell}></div>
)
} else if (date > daysInMonth(currentMonth, currentYear)) {
return
} else {
const cellText = String(date)
date++
return (
<div key={cell} className={styles.cell}>{cellText}</div>
)
}
})}
</div>
)
})}
</div>
</div>
</>
)
}
export default Home
However, after my date
variable increments in the else
statement, I reset my count back to 1
at the end of every row.
How do I stop resetting my date
variable back to 1
at the end of every row?
Upvotes: 0
Views: 230
Reputation: 557
I wasn't quite happy with the accepted answer and its discussion, so here comes a rather lengthy answer to provide some additional context. I hope it might be helpful, if not then that's tough luck, I guess.
Logically, your problem is that you initialize your variable inside the [...Array(rows).keys()].map
callback (let date = 1
). Moving the initialization outside the loop will fix this logical problem.
Syntactically, you can't just move it outside of the map
call, because you're using JSX, which just provides syntactic sugar for a React.createElement(component, props, ...children)
call. While your map
call returns an array of JSX elements, and hence a valid argument for children
, a variable declaration using let
in its place will lead to a syntax error.
Extracting your rendering logic to a separate function in order to move the initialization before the loop, as shown by other answers, addresses this problem. Importantly, however, it does not extract the same logic but enables you to slightly change your logic (by moving the initialization outside of the loop). Also, the new function is not absolutely necessary, other syntactical hacks let you achieve the same thing, e.g.:
<div key={row} className={styles.row}>
{[0].map((e) => {
let date = 1;
return [...Array(cells).keys()].map((cell) => {
if (row === 0 && cell < firstDay) {
return <div key={cell} className={styles.cell}></div>;
} else if (date > daysInMonth(currentMonth, currentYear)) {
return;
} else {
const cellText = String(date);
date++;
return (
<div key={cell} className={styles.cell}>
{cellText}
</div>
);
}
});
})}
</div>;
I believe in your case, you could even just move the initialization to the top of your component.
The situation you ended up in is one of callback functions with side effects (the date
declared outside the callback is changed from within the callback), which often negatively impacts readability and maintainability of your code. JSX works well with a declarative approach and Functional Programming (as your use of map
shows).
Below an example of how you could achieve your goal by calculating number of the day without side effects.
To calculate which number to display in each cell, it relies on the cell index and factors in the offset of the specific month. The indexes used for the calculations of are provided by map
as the second parameter to the callback function. Step by step:
It calculates the days processed before the current week, derived from the current weekIndex
To that it adds the number days processed in the current week, derived from the current dayIndex
To that it adds +1
to arrive at the 1-based cell index. This is required because dayIndex
(like all indexes in JavaScript) is 0-based.
The 1-based cell index is only the correct day number if the first day of the month fell on a Sunday (offset
= 0). Therefore, the calculation factors in the offset of this month.
The result is a concise function with an inexpensive calculation:
const calculateDay = (weekIndex, dayIndex, offset) =>
7 * weekIndex + dayIndex + 1 - offset;
// Friday April 1 : 7 * 0 + 5 + 1 - 5 = 1
// Monday April 11: 7 * 2 + 1 + 1 - 5 = 11
// Sunday May 1 : 7 * 0 + 0 + 1 - 0 = 1
// Tuesday May 31 : 7 * 4 + 2 + 1 - 0 = 31
const {
useState
} = React;
const App = () => {
const today = new Date();
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
const currentYear = today.getFullYear();
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const daysInMonth = (iMonth, iYear) => {
return 32 - new Date(iYear, iMonth, 32).getDate();
};
const calculateDay = (weekIndex, dayIndex, offset) =>
7 * weekIndex + dayIndex + 1 - offset;
const renderCalendar = () => {
const totalDays = daysInMonth(currentMonth, currentYear);
const firstDayInMonth = new Date(currentYear, currentMonth).getDay();
const totalWeeks = Math.ceil((firstDayInMonth+totalDays) / 7);
return (
<div className="table">
<div className="row">
{weekDays.map((e) => (
<div className="column" key={`col${e}`}>
{e}
</div>
))}
</div>
{[...Array(totalWeeks)].map((w, wIx) => {
return (
<div className="row">
{weekDays.map((d, dIx) => (
<div className="column">
{(wIx === 0 && dIx < firstDayInMonth) ||
calculateDay(wIx, dIx, firstDayInMonth) > totalDays
? ""
: calculateDay(wIx, dIx, firstDayInMonth)}
</div>
))}
</div>
);
})}
</div>
);
};
return (
<div>
<h1>Calendar Budget App</h1>
<div>
<select
value={currentMonth}
onChange={(e) => setCurrentMonth(e.target.value)}
>
{months.map((m, i) => (
<option value={i}>{m}</option>
))}
</select>
<h2>
{months[currentMonth]} {currentYear}
</h2>
{renderCalendar()}
</div>
</div>
);
};
// Render it
ReactDOM.render( <App / > ,
document.getElementById("root")
);
.App {
font-family: sans-serif;
text-align: center;
}
.table {
margin: 48px 0;
box-shadow: 0 5px 10px -2px #cfcfcf;
font-family: Arial;
display: table;
width: 100%;
}
.row {
display: table-row;
min-height: 48px;
border-bottom: 1px solid #f1f1f1;
}
.column {
display: table-cell;
border: 1px solid #f1f1f1;
vertical-align: middle;
padding: 10px 0;
}
.row:first-child {
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 1
Reputation: 23
I like michmich112's answer but unfortunatly I can't comment on it (lack of reputation)
But you can improve the render time by adding useMemo
import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')
const Home: NextPage = () => {
const today = new Date()
const [currentMonth, setCurrentMonth] = useState(today.getMonth())
const [currentYear, setCurrentYear] = useState(today.getFullYear())
const calendarBodyRef = useRef<HTMLDivElement>(null)
const rows = 6
const cells = 7
const firstDay = (new Date(currentYear, currentMonth)).getDay()
// check how many days in a month code from https://dzone.com/articles/determining-number-days-month
const daysInMonth = (iMonth: number, iYear: number) => {
return 32 - new Date(iYear, iMonth, 32).getDate()
}
const renderCalendar = useMemo(() => {
let date = 1;
return [...Array(rows).keys()].map((row) => {
return (
<div key={row} className={styles.row}>
{[...Array(cells).keys()].map((cell) => {
if (row === 0 && cell < firstDay) {
return (
<div key={cell} className={styles.cell}></div>
)
} else if (date > daysInMonth(currentMonth, currentYear)) {
return
} else {
const cellText = String(date)
date++
return (
<div key={cell} className={styles.cell}>{cellText}</div>
)
}
})}
</div>
)
})}
}, [currentMonth, currentYear])
return (
<>
<Head>
<title>Calendar Budget App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.calendarWrap}>
<h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
<div className={styles.daysWrap}>
<span>Sun</span>
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
</div>
<div ref={calendarBodyRef} className={styles.calendarBody}>
{renderCalendar()}
</div>
</div>
</>
)
}
export default Home
Upvotes: 0
Reputation: 774
Easy answer, extract your print into another function and call it with your component's return statement. Also note that your print statement is O(n^2) so re-rendering with be a costly operation.
Code:
import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')
const Home: NextPage = () => {
const today = new Date()
const [currentMonth, setCurrentMonth] = useState(today.getMonth())
const [currentYear, setCurrentYear] = useState(today.getFullYear())
const calendarBodyRef = useRef<HTMLDivElement>(null)
const rows = 6
const cells = 7
const firstDay = (new Date(currentYear, currentMonth)).getDay()
// check how many days in a month code from https://dzone.com/articles/determining-number-days-month
const daysInMonth = (iMonth: number, iYear: number) => {
return 32 - new Date(iYear, iMonth, 32).getDate()
}
const renderCalendar = () => {
let date = 1;
return [...Array(rows).keys()].map((row) => {
return (
<div key={row} className={styles.row}>
{[...Array(cells).keys()].map((cell) => {
if (row === 0 && cell < firstDay) {
return (
<div key={cell} className={styles.cell}></div>
)
} else if (date > daysInMonth(currentMonth, currentYear)) {
return
} else {
const cellText = String(date)
date++
return (
<div key={cell} className={styles.cell}>{cellText}</div>
)
}
})}
</div>
)
})}
}
return (
<>
<Head>
<title>Calendar Budget App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.calendarWrap}>
<h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
<div className={styles.daysWrap}>
<span>Sun</span>
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
</div>
<div ref={calendarBodyRef} className={styles.calendarBody}>
{renderCalendar()}
</div>
</div>
</>
)
}
export default Home
Upvotes: 1