dev_el
dev_el

Reputation: 2947

Why does my date variable reset in my map

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.

enter image description here

How do I stop resetting my date variable back to 1 at the end of every row?

Upvotes: 0

Views: 230

Answers (3)

tgikf
tgikf

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:

  1. It calculates the days processed before the current week, derived from the current weekIndex

  2. To that it adds the number days processed in the current week, derived from the current dayIndex

  3. 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.

  4. 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

Patrick Rutherford
Patrick Rutherford

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

michmich112
michmich112

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

Related Questions