Juan Angel Suarez
Building a React Cron Job Scheduler Component

Building a React Cron Job Scheduler Component

Introduction

In modern web applications, scheduling tasks to run at specific times or intervals is essential for operations like database maintenance, sending out newsletters, or running cleanup scripts. While several libraries exist for managing cron jobs in Node.js environments, creating a user-friendly, customizable scheduler directly in React can enhance your application's administrative capabilities. This post details building a React component for scheduling cron jobs, tailored to unique needs that existing solutions did not meet.

Problem

As I developed a feature-intensive application, I needed a scheduler that could offer not just basic cron job scheduling but also complex configurations such as selecting specific days of the week or times of the day. Unfortunately, existing React libraries were either too basic or not flexible enough to meet these specific user requirements.

Solution

To address this, I built a custom React componentCronScheduler. This component not only meets the diverse needs but also integrates smoothly with the React ecosystem.

Overview

Cron jobs are an essential feature of any backend system that requires tasks to be run at specific intervals. Here I will walk you through the process of building a Cron Scheduler component in React that allows users to configure job frequencies.

Preview

Maple
Lake
Mountains

The React Cron Scheduler Component

Our CronScheduler component will allow users to select the frequency of the task (daily, weekly, or monthly), specify the time of day to run the task, and choose specific days of the week or month if needed. It also has the ability to be enabled or disabled by the parent component.

Prerequisites

Before diving into the code, ensure you have a basic understanding of React and its hooks system (useState, useEffect, and useRef), as well as a working React environment.

Step 1: Setting Up the Component

Start by importing necessary hooks and CSS for styling:

import React, { useState, useEffect, useRef, useCallback } from 'react'
import './CronScheduler.css'

CSS File

.cron-scheduler {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  background-color: #ffffff;
  padding: 1rem;
  border-radius: 5px;
  width: 300px;
}

.cron-scheduler select,
.cron-scheduler input,
.cron-scheduler button {
  padding: 10px;
  border: 1px solid #ebebeb;
  border-radius: 5px;
  background-color: #ffffff;
}

.cron-scheduler button {
  background-color: #ff5a5f;
  color: #ffffff;
  cursor: pointer;
}

.day-selector {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.day {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: 5px;
  border: 1px solid #ebebeb;
}

.day.disabled {
  background-color: #ebebeb;
  color: #9e9e9e;
  cursor: not-allowed;
  border: 1px solid #ebebeb;
}

.day.selected {
  background-color: #ff5a5f;
  color: #ffffff;
}

Step 2: Initializing State

Use the useState hook to initialize the component's state based on props. In this case the variable p is the props passed down to the component.

const [frequency, setFrequency] = useState(p.frequency)
const [time, setTime] = useState(`${p.hour}:${p.minute}`)
const [days, setDays] = useState(p.days)
const [months, setMonths] = useState(p.months)

Step 3: Using Refs to Store Previous State Values

useRef is utilized to keep track of previous state values for comparison purposes:

const prevFrequency = useRef(p.frequency)
const prevTime = useRef(`${p.hour}:${p.minute}`)
const prevDays = useRef(p.days)
const prevMonths = useRef(p.months)

Step 4: Handling State Changes and Effects

Define a handleSave function using useCallback to bundle the state into an object and trigger a callback (p.onSave) with this object. The (p.onSave) method is a function from the parent component that updates the value of the state variable frequency in the parent component:

const handleSave = useCallback(() => {
  let [hour, minute] = time.split(':')
  let day_of_week = days?.filter((i) => i !== '').join(',')
  let day_of_month = months.filter((i) => i !== '').join(',')
  let frequencyObject = {
    type: frequency,
    hour: hour,
    minute: minute,
    day_of_week: day_of_week,
    day: day_of_month,
  }
  // Call the onSave prop with the frequency object
  p.onSave(frequencyObject)
}, [time, days, months, frequency, p])

Use useEffect to detect changes in state and perform actions accordingly. This includes comparing current state with previous state and updating refs:

useEffect(() => {
  // Function to compare arrays
  const arraysEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b)

  // Check if any value is different from its previous value
  if (
    frequency !== prevFrequency.current ||
    time !== prevTime.current ||
    !arraysEqual(days, prevDays.current) ||
    !arraysEqual(months, prevMonths.current)
  ) {
    handleSave()
  }

  // Update refs with current state
  prevFrequency.current = frequency
  prevTime.current = time
  prevDays.current = days
  prevMonths.current = months
}, [frequency, time, days, months, handleSave])

Step 5: Building the UI

Create the component's UI, incorporating conditional rendering to display elements based on the frequency state:

return (
  <div className="cron-scheduler">
    {/* Frequency selector */}
    {/* Time input */}
    {/* Conditional rendering for "weekly" and "monthly" specific UI */}
  </div>
)
  • Frequency Selector: A dropdown to select the frequency (daily, weekly, monthly).
  • Time Input: An input field for selecting time.
  • Day/Month Selectors: Conditional UI elements that appear based on the selected frequency.

Putting It All Together

import React, { useState, useEffect, useRef, useCallback } from 'react'
import './CronScheduler.css'

const CronScheduler = (p) => {
  const [frequency, setFrequency] = useState(p.frequency)
  const [time, setTime] = useState(`${p.hour}:${p.minute}`)
  const [days, setDays] = useState(p.days)
  const [months, setMonths] = useState(p.months)

  // Refs to store the previous state values
  const prevFrequency = useRef(p.frequency)
  const prevTime = useRef(`${p.hour}:${p.minute}`)
  const prevDays = useRef(p.days)
  const prevMonths = useRef(p.months)

  console.log('Frequency: ', frequency)
  console.log('Days: ', days)
  console.log('Months: ', months)

  const handleSave = useCallback(() => {
    let [hour, minute] = time.split(':')
    let day_of_week = days?.filter((i) => i !== '').join(',')
    let day_of_month = months.filter((i) => i !== '').join(',')
    let frequencyObject = {
      type: frequency,
      hour: hour,
      minute: minute,
      day_of_week: day_of_week,
      day: day_of_month,
    }
    // Call the onSave prop with the frequency object
    p.onSave(frequencyObject)
  }, [time, days, months, frequency, p])

  useEffect(() => {
    // Function to compare arrays
    const arraysEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b)

    // Check if any value is different from its previous value
    if (
      frequency !== prevFrequency.current ||
      time !== prevTime.current ||
      !arraysEqual(days, prevDays.current) ||
      !arraysEqual(months, prevMonths.current)
    ) {
      handleSave()
    }

    // Update refs with current state
    prevFrequency.current = frequency
    prevTime.current = time
    prevDays.current = days
    prevMonths.current = months
  }, [frequency, time, days, months, handleSave])
  return (
    <div className="cron-scheduler">
      <label>
        <b>Select Frequency:</b>
      </label>
      <select
        disabled={p.disabled}
        value={frequency}
        onChange={(e) => {
          setFrequency(e.target.value)
          setDays([])
          setMonths([])
        }}
      >
        <option value="daily">Daily</option>
        <option value="weekly">Weekly</option>
        <option value="monthly">Monthly</option>
      </select>

      <label>
        <b>Select Time:</b>
      </label>
      <input
        disabled={p.disabled}
        type="time"
        value={time}
        onChange={(e) => {
          console.log('TIME:', e.target.value)
          setTime(e.target.value)
        }}
      />

      {frequency === 'weekly' && (
        <div className={`day-selector ${p.disabled ? 'disabled' : ''}`}>
          {['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].map((day, index) => (
            <div
              key={index}
              className={`day ${days.includes(day) ? 'selected' : ''} ${
                p.disabled ? 'disabled' : ''
              }`}
              onClick={() => {
                if (!p.disabled) {
                  if (days.includes(day)) setDays((prevDays) => prevDays.filter((d) => d !== day))
                  else setDays((prevDays) => [...prevDays, day])
                }
              }}
            >
              {day}
            </div>
          ))}
        </div>
      )}

      {frequency === 'monthly' && (
        <div className={`day-selector ${p.disabled ? 'disabled' : ''}`}>
          {Array.from({ length: 31 }).map((_, index) => (
            <div
              key={index}
              className={`day ${months.includes(String(index + 1)) ? 'selected' : ''} ${
                p.disabled ? 'disabled' : ''
              }`}
              onClick={() => {
                if (!p.disabled) {
                  if (months.includes(String(index + 1))) {
                    setMonths((prevMonths) => prevMonths.filter((m) => m !== String(index + 1)))
                  } else {
                    setMonths((prevMonths) => [...prevMonths, String(index + 1)])
                  }
                }
              }}
            >
              {index + 1}
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

export default CronScheduler

Conclusion

We covered how to build a CronScheduler component in React, focusing on managing state with hooks, handling user input, and dynamically rendering UI elements based on state. If you have any questions or suggestions feel free to reach via linkedin.

Good luck building!