Open3

Common Sense Refactoring of a Messy React Component [Memo]

HiroHiro

Introduction

This scrap is a note of Common Sense Refactoring of a Messy React Component

Before dig in..
This is React component the author provided.

function Form() {
  const [formLink, setFormLink] = useState('')
  const [userPersona, setUserPersona] = useState('')
  const [startDate, setStartDate] = useState('')
  const [endDate, setEndDate] = useState('')
  const [numberOfVisits, setNumberOfVisits] = useState('')
  const [companyNumber, setCompanyNumber] = useState('')
  const [numberIncorrect, setNumberIncorrect] = useState(0)
  const [isFormValid, setIsFormValid] = useState(false)
  const [buttonText, setButtonText] = useState('Next')
  const [isProcessing, setIsProcessing] = useState(false)
  const [estimatedTime, setEstimatedTime] = useState('Enter number')
  const [recentActions, setRecentActions] = useState([])
  const [abortController, setAbortController] = useState(null)

  useEffect(() => {
    fetchPreviousActions()
  }, [])

  const fetchPreviousActions = async () => {
    try {
      const response = await fetch('https://api.com/actions', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      })

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const data = await response.json()
      data.sort(
        (a, b) => new Date(b.actiond_date) - new Date(a.actiond_date)
      )
      setRecentActions(data)
    } catch (error) {
      console.error('Failed to fetch recent actions', error)
    }
  }

  const [showOverlay, setShowOverlay] = useState(false)

  const renderLayout = () => (
    <div>
      <div>
        <div>Analyzing...</div>
        <button onClick={handleCancelaction}>Cancel</button>
      </div>
    </div>
  )

  const formatDate = (dateStr) => {
    return dateStr.replace(/-/g, '')
  }

  const callBackendAPI = async (formData) => {
    const controller = new AbortController()
    setAbortController(controller)
    formData.startDate = formatDate(formData.startDate)
    formData.endDate = formatDate(formData.endDate)

    try {
      const response = await fetch('https://api.com/action', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
        signal: controller.signal,
      })

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const data = await response.json()
      setShowOverlay(false)
      window.open(
        'https://app.com/action/' + data.id,
        '_blank',
        'noopener,noreferrer'
      )
      window.location.reload()
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Scraping halted')
      } else {
        console.error('Failed to call the API', error)
      }
    } finally {
      setShowOverlay(false)
      setIsProcessing(false)
    }
  }

  const handleCancelaction = () => {
    if (abortController) {
      abortController.abort() // Abort the fetch request
    }
    setShowOverlay(false)
    setIsProcessing(false)
  }

  useEffect(() => {
    if (!recentActions) {
      fetchPreviousActions()
    }

    setIsFormValid(startDate && endDate && endDate > startDate)
  }, [numberOfVisits, startDate, endDate])

  const handleSubmit = async (event) => {
    event.preventDefault()
    if (!isFormValid) return

    setShowOverlay(true)
    setIsProcessing(true)

    // Construct the form data object
    const formData = {
      userPersona,
      startDate,
      endDate,
      numberOfVisits: parseInt(numberOfVisits, 10),
    }
    // Calling the API with the form data
    await callBackendAPI(formData)
    setIsProcessing(false)
  }

  const handleSubmitCompanyNumber = (number) => {
    // this is unneeded, we've already set the value in state
    setCompanyNumber(number)
    if (number.length < 9) setNumberIncorrect(1)
    else setNumberIncorrect(0)
  }

  return !numberIncorrect ? (
    <div>
      <div>
        <img src={require('../imgs/LogoWhite.png')} alt="Logo" />
      </div>
      <div>
        <div>Tool</div>
        <form onSubmit={handleSubmit}>
          <label htmlFor="company_number">
            Enter your credentials
          </label>
          <input
            type="text"
            name="company_number"
            id="company_number"
            placeholder="Company Number"
            value={companyNumber}
            onChange={(e) => setCompanyNumber(e.target.value)}
          />
          <button
            type="submit"
            onClick={(e) => handleSubmitCompanyNumber(companyNumber)}
          >
            <span>Login</span>
            <span>&gt;</span>
          </button>
          {numberIncorrect > 0 ? (
            <span>The number you entered is incorrect</span>
          ) : (
            ''
          )}
        </form>
      </div>
    </div>
  ) : (
    <div>
      <div>
        <img
          src={require('../imgs/LogoWhite.png')}
          style={{ width: '200px', marginTop: '50px' }}
          alt="Logo"
        />
      </div>
      <div>
        <div>
          <div>New action</div>
          <form style={{ marginTop: '3vh' }} onSubmit={handleSubmit}>
            <div>
              <label>
                Visits
                <span
                  style={{
                    color: 'gray',
                    fontWeight: 'lighter',
                  }}
                >
                  (optional)
                </span>
              </label>
              <input
                type="number"
                value={numberOfVisits}
                onChange={(e) => setNumberOfVisits(e.target.value)}
              />
              <label className="form-label">
                Define a user persona{' '}
                <span
                  style={{
                    color: 'gray',
                    fontWeight: 'lighter',
                  }}
                >
                  (optional)
                </span>
              </label>
              <input
                type="text"
                id="posts-input"
                value={userPersona}
                onChange={(e) => setUserPersona(e.target.value)}
              />
            </div>
            <label
              className="form-label"
              style={{ textAlign: 'left' }}
            >
              Time period{' '}
              <span
                style={{
                  color: 'gray',
                  fontWeight: 'lighter',
                }}
              >
                (available for dates before June 2023)
              </span>
            </label>

            <div id="time-input">
              <input
                type="date"
                style={{ marginRight: '20px' }}
                value={startDate}
                onChange={(e) => setStartDate(e.target.value)}
              />
              <span style={{ fontSize: '15px' }}>to</span>
              <input
                type="date"
                style={{ marginLeft: '20px' }}
                value={endDate}
                onChange={(e) => setEndDate(e.target.value)}
              />
            </div>
            <button
              type="submit"
              className={`next-button ${isFormValid ? 'active' : ''}`}
              disabled={!isFormValid || isProcessing}
            >
              <span>Begin</span>
              <span></span>
            </button>
          </form>
        </div>
        <div id="divider"></div>

        <div>
          <div>Recents</div>
          <div>
            <div>
              {recentActions.map((action, index) => (
                <div key={index}>
                  <a href={action.link} target="_blank">
                    <span>r/{action.obfuscated}</span>{' '}
                    <span>{action.actiond_date} (UTC)</span>
                  </a>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
      {showOverlay ? renderLayout() : null}
    </div>
  )
}

HiroHiro

Starting With a Test

Do not delete code

No matter how unimportant it may seem, until you fully understand it.

Try wirting some tests, if there are no test codess.

Remember that refactoring is the process of changing the design of the code without altering it behaviour.

Focus on black-box testing

Focus only on testes that evaluate the component as black box and validate the end result. Make sure that it calls the correct callback functions it receives as props and that it renders everything correctly. How exactly it does this is not the concern at this stage.

HiroHiro

About Test for React Components

*not in the article

Choose Testing Libraries Wisely

Select appropriate libraries for testing needs, such as

Write Different Types of Tests

  • Unit Tests: Validate individual functionalities of components to ensure they work as intended in isolation.

  • Snapshot Tests: Capture the rendered output of components and compare it against stored snapshots to detect unintended changes.

  • Integration Tests: Ensure that multiple components work together correctly, validating their interactions and data flow.

Emphasize User-Centric Testing

Prioritize tests that reflect user interactions and experiences. Focus on how the component behaves from the user's perspective, ensuring it responds correctly to events and renders the expected output. The internal implementation details can be addressed later in the testing process.