2
\$\begingroup\$

I've created a Node.js/Express application that accepts personal data in a form on the front end with React (name, address, etc). It's purpose is to take a user's name, address, and some other info and use it to display a formal letter (Dear Mr John Doe...) with the user's info automatically filled in.

The form data is submitted with front and backend validation utilizing a proxy and SmartyStreets API to check if the address is real.

Here is a typical success use case...

  1. User fills out form and clicks submit.
  2. A modal window displays stating that the address in being verified.
  3. Another modal displays with a list of possible addresses.
  4. The user clicks their address from the list and submits.
  5. The formal letter is displayed with the user's info filled in.

I am a beginner and this is my first React app (written with hooks and function components), though it's been refactored many times.

I'm looking to improve this application according to SOLID principles (or have my assumptions corrected if this is really about functional programming) as it relates to the functions of the React front end. I also am looking for advice on how useReducer might compare to useState for improved readability, reliability, and debugging with specific code examples if possible. I realize this would be a lot of code to look over so I'll leave out the backend code and display the React components instead. I'll also provide the link to the github repo.

https://github.com/kylewcode/react-html-letter-v2

App.js

import axios from "axios";
import React, { Fragment, useState } from "react";
import { stateList } from "./utils/stateList";
import Modal from "./Modal";
import Display from "./Display";

// Bootstrap
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import ModalContainer from "react-bootstrap/Modal";
import Spinner from "react-bootstrap/Spinner";
import Alert from "react-bootstrap/Alert";

// The purpose of this component is to maintain the global state and determine the rendering of all other components.
function App() {
  const statesJSX = [];
  for (const [index, value] of stateList.entries()) {
    statesJSX.push(
      <option key={index} value={value}>
        {value}
      </option>
    );
  }

  const initState = {
    firstName: "",
    lastName: "",
    street: "",
    aptSuite: "",
    city: "",
    stateName: "",
    zipcode: "",
    date: "",
    title: "",
    status: "fillingOutForm",
  };

  const [state, setState] = useState(() => initState);
  const [responseData, setResponseData] = useState(() => null);
  const [showInvalidError, setShowInvalidError] = useState(false);
  const [showSystemError, setShowSystemError] = useState(false);

  function setStateByObject(object) {
    setState({ ...state, ...object });
  }

  function setStateByKeyValue(key, value) {
    setState({ ...state, [key]: value });
  }

  async function handleSubmit(e) {
    // Purpose #1 Submit form data to API request
    e.preventDefault();

    setStateByKeyValue("status", "validatingData");

    // Front end handles validation and all form inputs except apt/suite are required
    const requestBody = {
      street: state.street,
      city: state.city,
      state: state.stateName,
      zipcode: state.zipcode,
    };

    if (state.aptSuite) requestBody.secondary = state.aptSuite;

    const dataValidated = await requestData(requestBody);

    // Purpose#2 Update the state to trigger Modal render with trimmed whitespace data otherwise the rendered letter will be formatted incorrectly. I don't currently know how I would keep onSubmit to a single purpose.
    const inputs = e.target.elements;
    const newState = trimTextInputs(inputs);

    // Modal needs to be displayed after form is submitted.
    if (dataValidated) newState.status = "formSubmitted";
    setStateByObject(newState);
  }

  function trimTextInputs(inputs) {
    let trimmed;
    let name;
    const result = {};
    for (let i = 0; i < inputs.length; i++) {
      if (inputs[i].nodeName === "INPUT" && inputs[i].type === "text") {
        trimmed = inputs[i].value.trim();
        name = inputs[i].name;
      }
      // Object is needed in order to update state properly because the key/name matches the state prop.
      // Ex: name(firstName) state prop is firstName
      result[name] = trimmed;
    }
    return result;
  }

  /* Address Validation */
  async function requestData(body) {
    try {
      const config = {
        headers: {
          "Content-Type": "application/json",
        },
      };
      // What backound route would I be posting to with Heroku?
      const res = await axios.post("http://localhost:5000", body, config);

      const isValidated = validateData(res.data);

      // Because of how the data from the request is scoped here and invoked in handleSubmit, React state is the only option I can think of that can pass the data to the Modal component in the App render.
      if (isValidated) {
        setResponseData(res.data);
        return true;
      } else {
        setResponseData(null);
        return false;
      }
    } catch (err) {
      console.log(err);
    }
  }

  function validateData(data) {
    if (Array.isArray(data) && data.length === 0) {
      // If SmartyStreets gets an invalid address it sends back an empty array. This triggers an error to notify the user.
      setShowInvalidError(true);
      return false;
    }

    if (!Array.isArray(data)) {
      // This error is just in case the API sends data that's completely wrong and notifies user.
      setShowSystemError(true);
      return false;
    }
    return true;
  }

  const formElement = (
    <Fragment>
      <Row className="justify-content-center">
        <Col sm="auto">
          <h1 className="text-center">Formal Letter Generator</h1>
          <p className="text-center">All fields required except Apt/Suite #</p>
          {showSystemError ? (
            <Alert
              variant="danger"
              onClose={() => setShowSystemError(false)}
              dismissible
            >
              <p>
                There has been a system error. Please try again at a later time.
              </p>
            </Alert>
          ) : null}
        </Col>
      </Row>

      <Row className="justify-content-center">
        <Col sm="auto">
          <Form onSubmit={(e) => handleSubmit(e)}>
            <Form.Row>
              <Col sm="auto">
                <Form.Group>
                  <Form.Label htmlFor="user-first-name">First name</Form.Label>
                  <Form.Control
                    type="text"
                    name="firstName"
                    id="user-first-name"
                    value={state.firstName}
                    required
                    onChange={(e) => {
                      const key = e.target.name;
                      const value = e.target.value;
                      setStateByKeyValue(key, value);
                    }}
                  />
                </Form.Group>
              </Col>
              <Col sm="auto">
                <Form.Group>
                  <Form.Label htmlFor="user-last-name">Last name</Form.Label>
                  <Form.Control
                    type="text"
                    name="lastName"
                    id="user-last-name"
                    value={state.lastName}
                    required
                    onChange={(e) => {
                      const key = e.target.name;
                      const value = e.target.value;
                      setStateByKeyValue(key, value);
                    }}
                  />
                </Form.Group>
              </Col>
            </Form.Row>

            <Row className="p-3 border border-1 my-3 shadow-sm rounded">
              <fieldset>
                <legend>Mailing address</legend>
                {showInvalidError ? (
                  <Alert
                    variant="danger"
                    onClose={() => setShowInvalidError(false)}
                    dismissible
                  >
                    <p>
                      You entered an invalid address. Please enter your address
                      and submit the form again.
                    </p>
                  </Alert>
                ) : null}
                <Form.Row>
                  <Form.Group as={Col} sm="6">
                    <Form.Label htmlFor="user-street">Street</Form.Label>
                    <Form.Control
                      type="text"
                      name="street"
                      id="user-street"
                      value={state.street}
                      required
                      onChange={(e) => {
                        const key = e.target.name;
                        const value = e.target.value;
                        setStateByKeyValue(key, value);
                      }}
                    />
                  </Form.Group>

                  <Form.Group as={Col} sm="2">
                    <Form.Label htmlFor="user-apt-suite">Apt/Suite</Form.Label>
                    <Form.Control
                      type="text"
                      name="aptSuite"
                      id="user-apt-suite"
                      value={state.aptSuite}
                      onChange={(e) => {
                        const key = e.target.name;
                        const value = e.target.value;
                        setStateByKeyValue(key, value);
                      }}
                    />
                  </Form.Group>
                </Form.Row>

                <Form.Row>
                  <Form.Group as={Col} sm="auto">
                    <Form.Label htmlFor="user-city">City</Form.Label>
                    <Form.Control
                      type="text"
                      name="city"
                      id="user-city"
                      value={state.city}
                      required
                      onChange={(e) => {
                        const key = e.target.name;
                        const value = e.target.value;
                        setStateByKeyValue(key, value);
                      }}
                    />
                  </Form.Group>

                  <Form.Group as={Col} sm="auto">
                    <Form.Label htmlFor="state-select">State</Form.Label>
                    <Form.Control
                      as="select"
                      name="stateName"
                      id="state-select"
                      value={state.stateName}
                      required
                      onChange={(e) => {
                        const key = e.currentTarget.name;
                        const value = e.currentTarget.value;
                        setStateByKeyValue(key, value);
                      }}
                    >
                      <option>Select state</option>
                      {statesJSX}
                    </Form.Control>
                  </Form.Group>

                  <Form.Group as={Col} sm="2">
                    <Form.Label htmlFor="user-zipcode">Zipcode</Form.Label>
                    <Form.Control
                      type="text"
                      name="zipcode"
                      id="user-zipcode"
                      value={state.zipcode}
                      required
                      pattern="\d{5}"
                      onChange={(e) => {
                        const key = e.target.name;
                        const value = e.target.value;
                        setStateByKeyValue(key, value);
                      }}
                    />
                    <Form.Text>Zipcodes must be 5 numbers.</Form.Text>
                  </Form.Group>
                </Form.Row>

                <Form.Row>
                  <Form.Group as={Col} sm="auto">
                    <Form.Label htmlFor="date-input">Date</Form.Label>
                    <Form.Control
                      type="date"
                      name="date"
                      id="date-input"
                      value={state.date}
                      required
                      onChange={(e) => {
                        const key = e.target.name;
                        const value = e.target.value;
                        setStateByKeyValue(key, value);
                      }}
                    />
                  </Form.Group>
                </Form.Row>
              </fieldset>
            </Row>

            <Form.Row>
              <Form.Group as={Col} sm="auto">
                <Form.Label htmlFor="user-title">What's your title?</Form.Label>
                <Form.Control
                  htmlSize="1"
                  type="text"
                  name="title"
                  id="user-title"
                  value={state.title}
                  placeholder="Mr, Mrs, Ms, etc..."
                  required
                  onChange={(e) => {
                    const key = e.target.name;
                    const value = e.target.value;
                    setStateByKeyValue(key, value);
                  }}
                />
              </Form.Group>
            </Form.Row>

            <Form.Row
              as={Col}
              sm="auto"
              className="justify-content-center mb-3"
            >
              <Button variant="dark" type="submit" size="lg">
                Submit
              </Button>
            </Form.Row>
          </Form>
        </Col>
      </Row>
    </Fragment>
  );

  return (
    <Container className="my-md-3 border bg-light shadow rounded">
      {state.status === "fillingOutForm" ? formElement : null}
      {state.status === "validatingData" ? (
        <Fragment>
          <ModalContainer
            backdrop="static"
            centered
            show={state.status === "validatingData"}
          >
            <ModalContainer.Header>
              <ModalContainer.Title>Confirming address...</ModalContainer.Title>
            </ModalContainer.Header>
            <ModalContainer.Body>
              <Spinner animation="border" role="status">
                <span className="sr-only">Confirming address...</span>
              </Spinner>
            </ModalContainer.Body>
          </ModalContainer>
          {formElement}
        </Fragment>
      ) : null}
      {state.status === "formSubmitted" ? (
        <Fragment>
          <ModalContainer
            backdrop="static"
            centered
            show={state.status === "formSubmitted"}
          >
            <ModalContainer.Body>
              <Modal
                addressData={responseData}
                callParentState={(data) => setStateByObject(data)}
              />
            </ModalContainer.Body>
          </ModalContainer>
          {formElement}
        </Fragment>
      ) : null}
      {state.status === "formConfirmed" ? (
        <Display
          formData={state}
          callParentState={(key, value) => setStateByKeyValue(key, value)}
        />
      ) : null}
    </Container>
  );
}

export default App;

Modal.js

import React, { Fragment } from "react";

import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import ListGroup from "react-bootstrap/ListGroup";

// @purpose To allow the user to validate the address based off a list of choices and send the correct address back to App.
// @data SmartyStreets list of addresses
function Modal({ addressData, callParentState }) {
  const displayAddresses = (data) =>
    data.map((address, index) => {
      const fullAddress = `${address.deliveryLine1}, ${address.lastLine}`;
      const { cityName, state, zipCode } = address.components;
      return (
        <ListGroup key={index}>
          <ListGroup.Item>
            <Form.Check
              type="radio"
              name="address-validate"
              id={`validate-${index}`}
              value={fullAddress}
              label={fullAddress}
              data-street={address.deliveryLine1}
              data-city={cityName}
              data-state={state}
              data-zipcode={zipCode}
            />
          </ListGroup.Item>
        </ListGroup>
      );
    });

  return (
    <Fragment>
      <p>
        These are the addresses in our records that match what you have entered.
        Note that apartment or suite numbers are disregarded. Please select one
        of the following:
      </p>
      <Form
        onSubmit={(e) => {
          e.preventDefault();

          const inputs = e.target;
          for (let i = 0; i < inputs.length; i++) {
            const element = inputs[i];
            if (
              element.checked &&
              element.computedRole === "radio" &&
              element.value !== "none"
            ) {
              const { street, city, state, zipcode } = element.dataset;
              const data = {
                street: street,
                city: city,
                stateName: state,
                zipcode: zipcode,
                status: "formConfirmed",
              };
              callParentState(data);
            }
            if (
              element.checked &&
              element.computedRole === "radio" &&
              element.value === "none"
            ) {
              // User needs to return to the form if they don't see their address.
              const data = {
                status: "fillingOutForm",
              };
              window.alert("Returning to form. Please reenter your address.");
              callParentState(data);
            }
          }
        }}
      >
        {addressData === null ? null : displayAddresses(addressData)}
        <ListGroup>
          <ListGroup.Item>
            <Form.Check
              type="radio"
              name="address-validate"
              id="validate-none"
              value="none"
              label="I do not see my address here."
            />
          </ListGroup.Item>
        </ListGroup>
        <div className="text-center">
          <Button variant="primary" type="submit" className="mt-3">
            Submit
          </Button>
        </div>
      </Form>
    </Fragment>
  );
}

export default Modal;

Display.js

import React, { Fragment } from "react";
import "./Display.css";

import Button from 'react-bootstrap/Button'

function Display({
  callParentState,
  formData: {
    firstName,
    lastName,
    street,
    aptSuite,
    city,
    stateName,
    zipcode,
    date,
    title,
  },
}) {
  function handleClick() {
    callParentState("status", "fillingOutForm");
  }

  return (
    <Fragment>
      <address className="sender-column">
        <p>
          <b>Dr. Eleanor Gaye</b> <br />
          Awesome Science faculty <br />
          University of Awesome <br />
          Bobtown, CA 99999, <br />
          USA <br />
          <b>Tel</b>: 123-456-7890 <br />
          <b>Email</b>: [email protected]
        </p>
      </address>
      <div className="sender-column">
        <time dateTime="2016-01-20">{date}</time>
      </div>
      <address>
        <p>
          <b>
            {title} {firstName} {lastName}
          </b>
          <br />
          {street} {aptSuite ? `#${aptSuite}` : null}
          <br />
          {city}, {stateName} {zipcode}
          <br />
          USA
        </p>
      </address>

      <h1>
        Re: {title} {firstName} {lastName} university application
      </h1>

      <p>
        Dear {firstName},
        <br />
        <br />
        Thank you for your recent application to join us at the University of
        Awesome's science faculty to study as part of your
        <abbr title="A doctorate in any discipline except medicine, or sometimes theology.">
          {" "}
          PhD{" "}
        </abbr>
        next year. I will answer your questions one by one, in the following
        sections.
      </p>

      <h2>Starting dates</h2>

      <p>
        We are happy to accommodate you starting your study with us at any time,
        however it would suit us better if you could start at the beginning of a
        semester; the start dates for each one are as follows:
      </p>

      <ul>
        <li>
          First semester: <time dateTime="2016-09-09">9 September 2016</time>
        </li>
        <li>
          Second semester: <time dateTime="2016-01-15">15 January 2017</time>
        </li>
        <li>
          Third semester: <time dateTime="2017-05-02">2 May 2017</time>
        </li>
      </ul>
      <p>
        Please let me know if this is ok, and if so which start date you would
        prefer.
        <br />
        <br />
        You can find more information about{" "}
        <a href="http://example.com." target="_blank" rel="noreferrer">
          important university dates
        </a>
        on our website.
      </p>

      <h2>Subjects of study</h2>

      <p>
        At the Awesome Science Faculty, we have a pretty open-minded research
        facility — as long as the subjects fall somewhere in the realm of
        science and technology. You seem like an intelligent, dedicated
        researcher, and <strong>just </strong>
        the kind of person we'd like to have on our team. Saying that, of the
        ideas you submitted we were most intrigued by are as follows, in order
        of priority:
      </p>
      <ul>
        <li>
          Turning H<sub>2</sub>0 into wine, and the health benefits of
          Resveratrol (C<sub>14</sub>H<sub>12</sub>O<sub>3</sub>.)
        </li>
        <li>
          Measuring the effect on performance of funk bassplayers at
          temperatures exceeding 30°C (86°F), when the audience size
          exponentially increases (effect of 3 × 10<sup>3</sup>
          increasing to 3 × 10<sup>4</sup>.)
        </li>
        <li>
          <abbr title="HyperText Markup Language">HTML</abbr> and{" "}
          <abbr title="Cascading Style Sheets">CSS </abbr>
          constructs for representing musical scores.
        </li>
      </ul>
      <p>
        So please can you provide more information on each of these subjects,
        including how long you'd expect the research to take, required staff and
        other resources, and anything else you think we'd need to know? Thanks.
      </p>

      <h2>Exotic dance moves</h2>

      <p>
        Yes, you are right! As part of my post-doctorate work, I <em>did</em>{" "}
        study exotic tribal dances. To answer your question, my favourite dances
        are as follows, with definitions:
      </p>

      <dl>
        <dt>Polynesian chicken dance</dt>
        <dd>
          A little known but very influential dance dating back as far as 300
          <abbr title="Before Christ">BC</abbr>, a whole village would dance
          around in a circle like chickens, to encourage their livestock to be
          "fruitful".
        </dd>
        <dt>Icelandic brownian shuffle</dt>
        <dd>
          Before the Icelanders developed fire as a means of getting warm, they
          used to practice this dance, which involved huddling close together in
          a circle on the floor, and shuffling their bodies around in
          imperceptibly tiny, very rapid movements. One of my fellow students
          used to say that he thought this dance inspired modern styles such as
          Twerking.
        </dd>
        <dt>Arctic robot dance</dt>
        <dd>
          An interesting example of historic misinformation, English explorers
          in the 1960s believed to have discovered a new dance style
          characterized by "robotic", stilted movements, being practiced by
          inhabitants of Northern Alaska and Canada. Later on however it was
          discovered that they were just moving like this because they were
          really cold.
        </dd>
      </dl>
      <p>
        For more of my research, see my{" "}
        <a href="http://example.com." target="_blank" rel="noreferrer">
          exotic dance research page
        </a>
        .
        <br />
        <br />
        Yours sincerely,
        <br />
        <br />
        Dr Eleanor Gaye
        <br />
        <br />
        University of Awesome motto: <q>Be awesome to each other.</q> --
        <cite>
          The memoirs of Bill S Preston,
          <abbr title="An abbreviation for esquire."> Esq</abbr>
        </cite>
      </p>
      <div className="return-button">
        <h3>Made a mistake?</h3>
        <Button onClick={handleClick}>Click to return to form</Button>
      </div>
    </Fragment>
  );
}

export default Display;
\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.