Categories
React

React Styled Components — Hooks, Refs, and Security

React is the most used front end library for building modern, interactive front end web apps. It can also be used to build mobile apps.

In this article, we’ll look at how to access themes with hooks, referencing DOM elements with refs, and dealing with security.

Accessing Themes Using the useContext React Hook

We can use the useContext React hook with the ThemeContext to access theme properties with hooks.

For instance, we can do that as follows:

import React, { useContext } from "react";
import { ThemeProvider, ThemeContext } from "styled-components";

const baseTheme = {
  color: "green",
  backgroundColor: "white"
};

const Foo = ({ children }) => {
  const themeContext = useContext(ThemeContext);
  return <div style={themeContext}>{children}</div>;
};

export default function App() {
  return (
    <div>
      <ThemeProvider theme={baseTheme}>
        <Foo>foo</Foo>
      </ThemeProvider>
    </div>
  );
}

In the code above, we used useContext with ThemeContext passed in to access the theme. themeContext has the exact same properties and values as baseTheme . Therefore, we can pass themeContext straight in as the value of the style prop.

In App , we wrapped ThemeProvider with theme set to baseTheme so that we can access the theme properties that are in baseTheme .

Therefore, we’ll see that ‘foo’ is displayed in green.

The theme Prop

We can pass themes down to child components with the theme prop. This works with components created with styled-components. For instance, we can do that as follows:

import React from "react";
import styled, { ThemeProvider } from "styled-components";

const Button = styled.button`
  font-size: 12px;
  margin: 5px;
  padding: 6px;
  border-radius: 3px;

color: ${props => props.theme.main};
  border: 2px solid ${props => props.theme.main};
`;

const theme = {
  main: "green"
};

export default function App() {
  return (
    <div>
      <ThemeProvider theme={theme}>
        <Button theme={{ main: "blue" }}>Foo</Button>
        <ThemeProvider theme={theme}>
          <div>
            <Button>Baz</Button>
            <Button theme={{ main: "red" }}>Bar</Button>
          </div>
        </ThemeProvider>
      </ThemeProvider>
    </div>
  );
}

In the code above, we created a styled button with various styles. The colors are passed in from the theme and we access it with the prop.theme.main property.

Then in App , we use the theme prop to pass in different colors for some of the buttons. The Foo button is blue and the Bar button is red.

They will override the original color value that’s specified in theme , which is green .

Therefore, we’ll see buttons that have red, green or blue text border and color. Foo is blue, Baz is green, and Bar is red,

Refs

We can pass in refs to a styled component to access the DOM properties and methods for the given styled component.

For instance, we can do that as follows:

import React from "react";
import styled from "styled-components";

const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  background: greenyellow;
  border: none;
  border-radius: 3px;
`;

export default function App() {
  const inputRef = React.useRef();
  React.useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <div>
      <Input ref={inputRef} />
    </div>
  );
}

In the code above, we created a styled input called Input. Then we passed in a ref created with the useRef hook to Input .

Then in the useEffect callback, we call inputRef.current.focus(); to focus the element. The empty array in the 2nd argument of useEffect indicates that we load the callback when App first loads.

Therefore, when we first load the page, the Input will be focused on.

Photo by Icons8 Team on Unsplash

Security

Security is a concern when creating styled-components with styled-components because all the text is interpolated in our styled component if we pass them in via props, themes, or anywhere else.

Therefore, we should sanitize any input that’s passed in. For instance, the following would be bad:

import React from "react";
import styled from "styled-components";

const userInput = "/steal-money";

const ArbitraryComponent = styled.div`
  background: url(${userInput});
`;

export default function App() {
  return (
    <div>
      <ArbitraryComponent />
    </div>
  );
}

since we load the URL in our code, which isn’t good. Therefore, we should add some checks to avoid this from happening.

Conclusion

We can access themes with the useContext hook. Also, we can pass in the theme prop to pass in theme properties to override the values in the base theme.

styled-components will forward refs to our HTML element so that we can get the HTML element that’s in the styled component created with the package via the refs.

Finally, we should be careful when we’re interpolating strings in our styling code to avoid arbitrary code execution attacks.

Categories
Next.js

How to Build a Server Side Rendered React app with Next.js

React’s most popular code generator, Create React App, creates a client side rendered app, which means everyone that loads your page downloads the whole app and all front end code is ran on client side. This means that there is not as good for search engine optimization as JavaScript has to be run for web crawlers to index the page. It is also slower than rendering on client side because running JavaScript on client side is not very fast.

Setting up a server side rendered app with the default code generated by Create React App is not easy. You have to do lots of changes to the default code to get anything working. Getting it to work exactly the way you want is even harder, so developers came up with some solutions. One of the best options is Next.js.

In this story, we will build an address book app, which uses those libraries, plus React Bootstrap, which has great integration with those libraries above to create forms. To start we need to run Next.js CLI to scaffold the app. We run npx create-next-app to create the app project folder with the initial files. The app will have a home page to display the contacts and let us open a modal to add a contact. There will be a table that displays all the contacts and Edit and Delete buttons on each row to edit or delete each contact. The contacts will store in a central Redux store to store the contacts in a central place, making them easy to access. React Router will be used for routing. Contacts will be saved in the back end spawned using the JSON server package.

For form validation, then you need to use a third-party library. Formik and Yup work great together to allow us to take care of most form validation needs. Formik let us build the forms and display the errors, and handle form value changes, which is another thing we have to do all my hand otherwise. Yup let us write a schema for validating our form fields. It can check almost anything, with common validation code like email and required fields available as built-in functions. It can also check for fields that depend on other fields, like the postal code format depending on the country. Bootstrap forms can be used seamlessly with Formik and Yup.

Once that is done, we have to install some libraries. To install the libraries we mentioned above, we run npm i axios bootstrap formik react-bootstrap react-redux react-router-dom yup . Axios is the HTTP client that we use for making HTTP requests to back end. react-router-dom is the package name for the latest version of React Router.

Now that we have all the libraries installed, we can start building the app. We create a store folder and create a file called actionCreator.js in it and add the following:

import { SET_CONTACTS } from './actions';

const setContacts = (contacts) => {
    return {
        type: SET_CONTACTS,
        payload: contacts
    }
};

export { setContacts };

This is the action creator for creating the action for storing the contacts in the store.

We create another file called actions.js in the same folder and add:

const SET_CONTACTS = 'SET_CONTACTS';

export { SET_CONTACTS };

This just have the type constant for dispatching the action.

Then in the store folder, we create index.js and add:

import { contactsReducer } from "./reducers";
import { createStore, combineReducers } from "redux";

const addressBookApp = combineReducers({
  contacts: contactsReducer,
});

const makeStore = (initialState, options) => {
  return createStore(addressBookApp, initialState);
};

export { makeStore };

We will use it in the main entry point file, which will be called _app.js in the pages folder to allow us to use the same Redux store in this multi-page, server side rendered app.

Then we make a file called reducers.js , and add:

import { SET_CONTACTS } from './actions';

function contactsReducer(state = {}, action) {
    switch (action.type) {
        case SET_CONTACTS:
            state = JSON.parse(JSON.stringify(action.payload));
            return state;
        default:
            return state
    }
}

export { contactsReducer };

This is the reducer where we store the contacts that we dispatch by calling the prop provided by the mapDispatchToProps function in our components.

Next we add some code files to the pages folder.

In index.js , we replace what is existing with the following:

import React from "react";
import Head from "next/head";
import { Router, Route } from "react-router-dom";
import { createMemoryHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import HomePage from "./HomePage";
import { contactsReducer } from "../store/reducers";
import { Provider } from "react-redux";
import { createStore, combineReducers } from "redux";

const history = createHistory();

const addressBookApp = combineReducers({
  contacts: contactsReducer,
});

const store = createStore(addressBookApp);

const Home = () => (
  <div>
    <Head>
      <title>Address Book</title>
      <link
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css)"
        rel="stylesheet"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
        crossOrigin="anonymous"
      />
    </Head>
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>

    <style jsx>{`
      .App {
        text-align: center;
      }
    `}</style>
  </div>
);

export default Home;

This is where we add the navigation bar and show our routes routed by the React Router. In the styles element, we replace the existing code with:

.App {
  text-align: center;
}

to center some text.

Next we build our contact form. This is the most logic heavy part of our app. We create a file called ContactForm.js in the components folder and add:

import React from 'react';
import { Formik } from 'formik';
import Form from 'react-bootstrap/Form';
import Col from 'react-bootstrap/Col';
import InputGroup from 'react-bootstrap/InputGroup';
import Button from 'react-bootstrap/Button';
import * as yup from 'yup';
import { COUNTRIES } from '../helpers/exports';
import PropTypes from 'prop-types';
import { addContact, editContact, getContacts } from '../helpers/requests';
import { connect } from 'react-redux';
import { setContacts } from '../store/actionCreators';

const schema = yup.object({
  firstName: yup.string().required('First name is required'),
  lastName: yup.string().required('Last name is required'),
  address: yup.string().required('Address is required'),
  city: yup.string().required('City is required'),
  region: yup.string().required('Region is required'),
  country: yup.string().required('Country is required').default('Afghanistan'),
  postalCode: yup
    .string()
    .when('country', {
      is: 'United States',
      then: yup.string().matches(/^[0-9]{5}(?:-[0-9]{4})?$/, 'Invalid postal code'),
    })
    .when('country', {
      is: 'Canada',
      then: yup.string().matches(/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/, 'Invalid postal code'),
    })
    .required(),
  phone: yup
    .string()
    .when('country', {
      is: country => ["United States", "Canada"].includes(country),
      then: yup.string().matches(/^[2-9]d{2}[2-9]d{2}d{4}$/, 'Invalid phone nunber')
    })
    .required(),
  email: yup.string().email('Invalid email').required('Email is required'),
  age: yup.number()
    .required('Age is required')
    .min(0, 'Minimum age is 0')
    .max(200, 'Maximum age is 200'),
});

function ContactForm({
  edit,
  onSave,
  setContacts,
  contact,
  onCancelAdd,
  onCancelEdit,
}) {
  const handleSubmit = async (evt) => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addContact(evt);
    }
    else {
      await editContact(evt);
    }
    const response = await getContacts();
    setContacts(response.data);
    onSave();
  }

return (
    <div className="form">
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={contact || {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="firstName">
                  <Form.Label>First name</Form.Label>
                  <Form.Control
                    type="text"
                    name="firstName"
                    placeholder="First Name"
                    value={values.firstName || ''}
                    onChange={handleChange}
                    isInvalid={touched.firstName && errors.firstName}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.firstName}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="lastName">
                  <Form.Label>Last name</Form.Label>
                  <Form.Control
                    type="text"
                    name="lastName"
                    placeholder="Last Name"
                    value={values.lastName || ''}
                    onChange={handleChange}
                    isInvalid={touched.firstName && errors.lastName}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.lastName}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="address">
                  <Form.Label>Address</Form.Label>
                  <InputGroup>
                    <Form.Control
                      type="text"
                      placeholder="Address"
                      aria-describedby="inputGroupPrepend"
                      name="address"
                      value={values.address || ''}
                      onChange={handleChange}
                      isInvalid={touched.address && errors.address}
                    />
                    <Form.Control.Feedback type="invalid">
                      {errors.address}
                    </Form.Control.Feedback>
                  </InputGroup>
                </Form.Group>
              </Form.Row>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="city">
                  <Form.Label>City</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="City"
                    name="city"
                    value={values.city || ''}
                    onChange={handleChange}
                    isInvalid={touched.city && errors.city}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.city}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="region">
                  <Form.Label>Region</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Region"
                    name="region"
                    value={values.region || ''}
                    onChange={handleChange}
                    isInvalid={touched.region && errors.region}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.region}
                  </Form.Control.Feedback>
                </Form.Group>

<Form.Group as={Col} md="12" controlId="country">
                  <Form.Label>Country</Form.Label>
                  <Form.Control
                    as="select"
                    placeholder="Country"
                    name="country"
                    onChange={handleChange}
                    value={values.country || ''}
                    isInvalid={touched.region && errors.country}>
                    {COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}
                  </Form.Control>
                  <Form.Control.Feedback type="invalid">
                    {errors.country}
                  </Form.Control.Feedback>
                </Form.Group>

<Form.Group as={Col} md="12" controlId="postalCode">
                  <Form.Label>Postal Code</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Postal Code"
                    name="postalCode"
                    value={values.postalCode || ''}
                    onChange={handleChange}
                    isInvalid={touched.postalCode && errors.postalCode}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.postalCode}
                  </Form.Control.Feedback>
                </Form.Group>

<Form.Group as={Col} md="12" controlId="phone">
                  <Form.Label>Phone</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Phone"
                    name="phone"
                    value={values.phone || ''}
                    onChange={handleChange}
                    isInvalid={touched.phone && errors.phone}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.phone}
                  </Form.Control.Feedback>
                </Form.Group>

<Form.Group as={Col} md="12" controlId="email">
                  <Form.Label>Email</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Email"
                    name="email"
                    value={values.email || ''}
                    onChange={handleChange}
                    isInvalid={touched.email && errors.email}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.email}
                  </Form.Control.Feedback>
                </Form.Group>

<Form.Group as={Col} md="12" controlId="age">
                  <Form.Label>Age</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Age"
                    name="age"
                    value={values.age || ''}
                    onChange={handleChange}
                    isInvalid={touched.age && errors.age}
                  />

<Form.Control.Feedback type="invalid">
                    {errors.age}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ 'marginRight': '10px' }}>Save</Button>
              <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>Cancel</Button>
            </Form>
          )}
      </Formik>
    </div>
  );
}

ContactForm.propTypes = {
  edit: PropTypes.bool,
  onSave: PropTypes.func,
  onCancelAdd: PropTypes.func,
  onCancelEdit: PropTypes.func,
  contact: PropTypes.object
}

const mapStateToProps = state => {
  return {
    contacts: state.contacts,
  }
}

const mapDispatchToProps = dispatch => ({
  setContacts: contacts => dispatch(setContacts(contacts))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ContactForm);

We use Formik to facilitate building our contact form here, with our Boostrap Form component nested in the Formik component so that we can use Formik’s handleChange , handleSubmit , values , touched and errors parameters. handleChange is a function that let us update the form field data from the inputs without writing the code ourselves. handleSubmit is the function that we passed into the onSubmit handler of the Formik component. The parameter in the function is the data we entered, with the field name as the key, as defined by the name attribute of each field and the value of each field as the value of those keys. Notice that in each value prop, we have ||'' so we do not get undefined values and prevent uncontrolled form warnings from getting triggered.

To display form validation messages, we have to pass in the isInvalid prop to each Form.Control component. The schema object is what Formik will check against for form validation. The argument in the required function is the validation error message. The second argument of the matches , min and max functions are also validation messages.

The parameter of the ContactForm function are props, which we will pass in from the HomePage component that we will build later. The handleSubmit function checks if the data is valid, then if it is then it will proceed to saving according to whether it is adding or editing a contact. Then when saving is successful we set the contacts in the store and call onSave prop, which is a function to close the modal the form is in. The modal will be defined in the home page.

mapStateToProps is a function provided by React Redux so that we can map the state directly to the props of our component as the function name suggests. mapDispatchToProps allows us to call function in the props of the component called setContacts to dispatch the action as we defined in actionCreators.js .

Next we create a file called exports.js after creating the helpers folder, and put:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; Herzegovina",
  "Botswana",
  "Brazil",
  "British Virgin Islands",
  "Brunei",
  "Bulgaria",
  "Burkina Faso",
  "Burundi",
  "Cambodia",
  "Cameroon",
  "Canada",
  "Cape Verde",
  "Cayman Islands",
  "Chad",
  "Chile",
  "China",
  "Colombia",
  "Congo",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Croatia",
  "Cruise Ship",
  "Cuba",
  "Cyprus",
  "Czech Republic",
  "Denmark",
  "Djibouti",
  "Dominica",
  "Dominican Republic",
  "Ecuador",
  "Egypt",
  "El Salvador",
  "Equatorial Guinea",
  "Estonia",
  "Ethiopia",
  "Falkland Islands",
  "Faroe Islands",
  "Fiji",
  "Finland",
  "France",
  "French Polynesia",
  "French West Indies",
  "Gabon",
  "Gambia",
  "Georgia",
  "Germany",
  "Ghana",
  "Gibraltar",
  "Greece",
  "Greenland",
  "Grenada",
  "Guam",
  "Guatemala",
  "Guernsey",
  "Guinea",
  "Guinea Bissau",
  "Guyana",
  "Haiti",
  "Honduras",
  "Hong Kong",
  "Hungary",
  "Iceland",
  "India",
  "Indonesia",
  "Iran",
  "Iraq",
  "Ireland",
  "Isle of Man",
  "Israel",
  "Italy",
  "Jamaica",
  "Japan",
  "Jersey",
  "Jordan",
  "Kazakhstan",
  "Kenya",
  "Kuwait",
  "Kyrgyz Republic",
  "Laos",
  "Latvia",
  "Lebanon",
  "Lesotho",
  "Liberia",
  "Libya",
  "Liechtenstein",
  "Lithuania",
  "Luxembourg",
  "Macau",
  "Macedonia",
  "Madagascar",
  "Malawi",
  "Malaysia",
  "Maldives",
  "Mali",
  "Malta",
  "Mauritania",
  "Mauritius",
  "Mexico",
  "Moldova",
  "Monaco",
  "Mongolia",
  "Montenegro",
  "Montserrat",
  "Morocco",
  "Mozambique",
  "Namibia",
  "Nepal",
  "Netherlands",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Nicaragua",
  "Niger",
  "Nigeria",
  "Norway",
  "Oman",
  "Pakistan",
  "Palestine",
  "Panama",
  "Papua New Guinea",
  "Paraguay",
  "Peru",
  "Philippines",
  "Poland",
  "Portugal",
  "Puerto Rico",
  "Qatar",
  "Reunion",
  "Romania",
  "Russia",
  "Rwanda",
  "Saint Pierre &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; Caicos",
  "Uganda",
  "Ukraine",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Uruguay",
  "Uzbekistan",
  "Venezuela",
  "Vietnam",
  "Virgin Islands (US)",
  "Yemen",
  "Zambia",
  "Zimbabwe",
];

These are countries for the countries field in the form.

Then we make a file called requests.js in the same folder, and add:

const APIURL = '[http://localhost:3000'](http://localhost:3000%27);
const axios = require('axios');

export const getContacts = () => axios.get(`${APIURL}/contacts`);

export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);

export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);

export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);

These are functions are making our HTTP requests to the back end to save and delete contacts.

In HomePage.js , we put:

import React from 'react';
import { useState, useEffect } from 'react';
import Table from 'react-bootstrap/Table'
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal'
import ContactForm from './ContactForm';
import './HomePage.css';
import { connect } from 'react-redux';
import { getContacts, deleteContact } from './requests';

function HomePage() {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedId, setSelectedId] = useState(0);
  const [selectedContact, setSelectedContact] = useState({});
  const [contacts, setContacts] = useState([]);

  const openModal = () => {
    setOpenAddModal(true);
  }

  const closeModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
    getData();
  }

  const cancelAddModal = () => {
    setOpenAddModal(false);
  }

  const editContact = (contact) => {
    setSelectedContact(contact);
    setOpenEditModal(true);
  }

  const cancelEditModal = () => {
    setOpenEditModal(false);
  }

  const getData = async () => {
    const response = await getContacts();
    setContacts(response.data);
    setInitialized(true);
  }

  const deleteSelectedContact = async (id) => {
    await deleteContact(id);
    getData();
  }

  useEffect(() => {
    if (!initialized) {
      getData();
    }
  })

  return (
    <div className="home-page">
      <h1>Contacts</h1>
      <Modal show={openAddModal} onHide={closeModal} >
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm edit={false} onSave={closeModal.bind(this)} onCancelAdd={cancelAddModal} />
        </Modal.Body>
      </Modal>

<Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm edit={true} onSave={closeModal.bind(this)} contact={selectedContact} onCancelEdit={cancelEditModal} />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      </ButtonToolbar>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Address</th>
            <th>City</th>
            <th>Country</th>
            <th>Postal Code</th>
            <th>Phone</th>
            <th>Email</th>
            <th>Age</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {contacts.map(c => (
            <tr key={c.id}>
              <td>{c.firstName}</td>
              <td>{c.lastName}</td>
              <td>{c.address}</td>
              <td>{c.city}</td>
              <td>{c.country}</td>
              <td>{c.postalCode}</td>
              <td>{c.phone}</td>
              <td>{c.email}</td>
              <td>{c.age}</td>
              <td>
                <Button variant="outline-primary" onClick={editContact.bind(this, c)}>Edit</Button>
              </td>
              <td>
                <Button variant="outline-primary" onClick={deleteSelectedContact.bind(this, c.id)}>Delete</Button>
              </td>
            </tr>
          ))}
        </tbody>
      </Table>
    </div>
  );
}

const mapStateToProps = state => {
  return {
    contacts: state.contacts,
  }
}

export default connect(
  mapStateToProps,
  null
)(HomePage);

It has the table to display the contacts and buttons to add, edit, and delete contacts. It gets data on the first load with the getData function call in the useEffect’s callback function. useEffect’s callback is called on every render so we want to set a initialized flag and check that it loads only if it’s true .

Note that we pass in all the props in this component to the ContactForm component. To pass an argument a onClick handler function, we have to call bind on the function and pass in the argument for the function as a second argument to bind . For example, in this file, we have editContact.bind(this, c) , where c is the contact object. The editContact function is defined as follows:

const editContact = (contact) => {
    setSelectedContact(contact);
    setOpenEditModal(true);
  }

c is the contact parameter we pass in.

In the styles block, we have:

.home-page {
  padding: 20px;
}

to add some padding.

In index.js in the pages folder, we replace the existing code with:

import React from "react";
import Head from "next/head";
import { Router, Route } from "react-router-dom";
import { createMemoryHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import HomePage from "./HomePage";
import { contactsReducer } from "../store/reducers";
import { Provider } from "react-redux";
import { createStore, combineReducers } from "redux";

const history = createHistory();

const addressBookApp = combineReducers({
  contacts: contactsReducer,
});

const store = createStore(addressBookApp);

const Home = () => (
  <div>
    <Head>
      <title>Address Book</title>
      <link
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css)"
        rel="stylesheet"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
        crossOrigin="anonymous"
      />
    </Head>
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>

    <style jsx>{`
      .App {
        text-align: center;
      }
    `}</style>
  </div>
);

export default Home;

This is the entry point of the app, we add the navigation menu here and also the route for the home page here. Since we do not have an index.html in a server side rendered app, we render the head tag of the page by putting the Head component here, along with the title and link components inside the Head . We include these to change the title and add the Bootstrap stylesheet respectively.

We have to add a file called _app.js in the pages folder to let us use our Redux store in all our components since this is not a normal client side rendered app. This file overrides the default App class in Next.js . The class is used for loading initialization code in our pages. There is no ReactDOM.render call with the topmost component where we can wrap the Provider component around it to let us use our Redux store everywhere, so we have to add:

// pages/_app.js
import React from "react";
import { Provider } from "react-redux";
import App, { Container } from "next/app";
import withRedux from "next-redux-wrapper";
import { contactsReducer } from "../store/reducers";
import { createStore, combineReducers } from "redux";

const addressBookApp = combineReducers({
  contacts: contactsReducer,
});

const makeStore = (initialState, options) => {
  return createStore(addressBookApp, initialState);
};

class MyApp extends App {
  static async getInitialProps({ Component, ctx }) {
    const pageProps = Component.getInitialProps
      ? await Component.getInitialProps(ctx)
      : {};

    return { pageProps };
  }

  render() {
    const { Component, pageProps, store } = this.props;
    return (
      <Container>
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    );
  }
}

export default withRedux(makeStore)(MyApp);

to wrap our Redux store around all our components. Component is any component in our app. We use the next-redux-wrapper package to pass the store object down to all of our components by allowing us to wrap Provider with the store prop around our components.

Now we can run the app by running npm run build then npm run start -- --port 3001 .

To start back end, we first install the json-server package by running npm i json-server . Them go to our project folder and run:

json-server --watch db.json

In db.json , change the text to:

{
  "contacts": [
  ]
}

so that we have the contacts endpoints defined in requests.js available.

At the end, we have the following:

Categories
Testing

JavaScript Unit Test Best Practices — UI Tests

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Query HTML Elements Based on Attributes that are Unlikely to Change

We should only check for HTML elements with attributes that aren’t likely to change.

This way, we don’t have to update our tests when we make small changes.

Also, this ensures that look and feel changes don’t break our tests.

For example, instead of writing:

test("whenever no data is passed, number of messages shows zero", () => {
  // ...
  expect(wrapper.find("[className='mr-2']").text()).toBe("0");
});

to test:

<span id="metric" className="mr-2">{value}</span>

Instead, we add our own ID to the element that we want to test and then use that in our test.

For instance, if we have:

<h3>
  <Badge pill className="fixed_badge" variant="dark">
    <span data-testid="msgs-label">{value}</span>
  </Badge>
</h3>

We can test it with:

test("whenever no data is passed, number of messages shows zero", () => {
  const metricValue = undefined;
  const { getByTestId } = render(<dashboardMetric value={undefined} />);
  expect(getByTestId("msgs-label").text()).toBe("0");
});

We shouldn’t rely on CSS attributes that can change at any time.

Instead, we add an ID that rarely or never changes.

Test with a Realistic and Fully Rendered Component

We should test with realistic and fully rendered components.

This way, we can trust our test that it’s actually testing the stuff in the component.

If we mock or do partial or shallow rendering, we may miss things in our tests.

If it’s too slow to test with the real thing, then we can consider mocks.

For instance, instead of shallow rendering with shallow :

test("when click to show filters, filters are displated", () => {
  const wrapper = shallow(<Calendar showFilters={false} title="Select Filter" />);
  wrapper
    .find("FiltersPanel")
    .instance()
    .showFilters();

  expect(wrapper.find("Filter").props()).toEqual({ title: "Select Filter" });

});

We write:

test("when click to show filters, filters are displated", () => {
  const wrapper = mount(<Calendar showFilters={false} title="Select Filter" />);
  wrapper.find("button").simulate("click");
  expect(wrapper.text().includes("Select Filter"));
});

We call mount to mount the Calendar component fully.

Then we do the click on the button as we do like a real user.

Then we check the text that should appear.

Use Frameworks Built-in Support for Async Events

We should test frameworks built-in async events when we’re running our tests.

This way, we actually wait for what we want to appear before running something.

Sleeping for a fixed time isn’t reliable and doesn’t help with waiting for items to appear before doing what we want.

This means our tests would be flaky.

Also, sleeping for a fixed time is a lot slower.

For instance, with Cypress, we can write:

cy.get("#show-orders").click();
cy.wait("@orders");

We wait for orders to appear when we click on the element with ID show-orders .

What we don’t want is to have code that waits with our own logic with setInterval :

test("user name appears", async () => {
  //...
  const interval = setInterval(() => {
    const found = getByText("james");
    if (found) {
      clearInterval(interval);
      expect(getByText("james")).toBeInTheDocument();
    }
  }, 100);

  const movie = await waitForElement(() => getByText("james"));
});

This is complex and we don’t take advantage of the full capabilities of test frameworks.

Conclusion

We should wait for things with test frameworks’ wait functions.

Also, we should test with realistic components.

Categories
Testing

JavaScript Unit Test Best Practices — Testing Behavior

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Test the Behavior and not the Internal Implementation

We should only testy the result and not worry about the internal implementation.

This way, we won’t worry ourselves with something that doesn’t need to be checked in tests.

For instance, we shouldn’t test internal variables:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');

  expect(userManager._users[0].name).toBe('james');
  expect(userManager._users[0].password).toBe('password');
});

Instead, we write:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');
  expect(userManager.login('james', 'password')).toBe(true);
});

We just test the returned results instead of internal variables.

This way, we don’t have to change our tests when the implementation changes.

Don’t Mock Everything

We shouldn’t mock everything.

This way, at least we’re testing something.

For instance, we can write:

describe('when the user has already visited the page', () => {
  describe('when the message is available', () => {
    it('should display the message', () => {
      const storage = new MemoryStorage();
      storage.setItem('page-visited', '1');
      const messageManager = new MessageManager(storage);
      spyOn(messageManager, 'display');
      messageManager.start();
      expect(messageManager.display).toHaveBeenCalled();
    });
  });
});

We use a memory storage solution instead of real local storage.

This way, our test doesn’t commit any side effects with our test.

We didn’t mock messageManager.display since we just want to check it’s called.

We use real versions of objects if it’s simple to set up.

They also shouldn’t create shared states between tests.

The speed of the real object should be fast if it’s used.

The real object also shouldn’t make any network requests or browser page reloads.

Create New Tests for Every Defect

There should be new tests for all defects that are fixed.

This way, we can fix it and never have it appear again in the same form.

Don’t Write Unit Tests for Complex User Interactions

Unit tests should be used to test simple actions.

If we have more complex workflows we want to test, then we should add integration or end to end tests.

They’re all needed for more complex workflows like filling forms and submitting data, etc.

Functional tests can be written with frameworks like Selenium or Cypress.

Test Simple User Actions

We should test simple user actions like clicks and inputs.

For example, we can write:

describe('clicking on the "Preview profile" link', () => {
  it('should show the profile preview if it is hidden', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: false });
    spyOn(profileModule, 'show');
    button.click(previewLink);
    expect(profileModule.show).toHaveBeenCalled();
  });

  it('should hide the profile preview if it is displayed', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: true });
    spyOn(profileModule, 'hide');
    button.click();
    expect(profileModule.hide).toHaveBeenCalled();
  });
});

We have the profileModule with various states and we do the click on each.

Then we check which function is called.

Review Test Code

Test code should be looked at so that we know the intent of the developer quickly.

Conclusion

We should test simple behavior in our tests.

Also, we shouldn’t mock everything to have more realistic tests.

Categories
Testing

JavaScript Unit Test Best Practices — Test Data and Logic

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Test Middlewares in Isolation

We should test middleware in isolation so that we can test the critical parts of our app.

Middlewares are used by everything, so we should test them.

Since they’re functions, we can test them in isolation.

We can just invoke it directly.

Also, we can use node-mock-http to spy on the behavior of the middleware.

We can write:

const unitUnderTest = require("./middleware");  
const httpMocks = require("node-mocks-http");  
  
test("should return http status 403 when request header is empty", () => {  
  const request = httpMocks.createRequest({  
    method: "GET",  
    url: "/user/1",  
    headers: {  
      authentication: ""  
    }  
  });  
  const response = httpMocks.createResponse();  
  unitUnderTest(request, response);  
  expect(response.statusCode).toBe(403);  
});

We use the node-mocks-http to make our requests.

Then we can pass the request and response objects into our middleware.

And then get the response.

Measure and Refactor Using Static Analysis Tools

We should use static analysis tools to improve our code quality and keep our code maintainable.

It’s useful for detecting duplication, code complexity, and other issues.

We can use tools like Sonarqube and Code Climate to check the statically analyze our app.

Check Our Readiness for Node-Related Chaos

We should also check for issues like when the server or a process dies, or when memory is overloaded.

Speed issues are also important to check.

To regularly free up resources, we can use tools to regularly kill our app instances.

We can do performance testing to find those issues.

APM tools will also help us find them.

Avoid Global Test Fixtures and Seeds

Global test fixtures aren’t good since they’re shared between multiple tests.

We don’t want to share them between multiple tests since we want independent tests.

Tests shouldn’t start with data that have been manipulated by other pieces of code.

Therefore, we should reset the data after each test so that we can always test with clean data.

Our tests should be able to run in any order and still give us the same results.

They also keep our tests simple since they’ll be easier to trace.

For instance, we write something like:

it("should return true if we can log in with valid username and password", async () => {  
  const username = 'james';  
  const password = 'password';  
  await addUser({ username, password });  
  const result = login(username, password);  
  expect(result).to.be(true);  
});

We create a new user and then use that to log in so that we know the user that we’re testing with exists.

Frontend Testing

Front end testing is also important.

Separate UI from Functionality

We should test UI separately from functionality.

When we’re testing component logic, then UI becomes the noise that should be extracted.

This way, our tests can focus on only manipulating data.

UI can be tested separately.

We can disable animation and only check data in tests.

Conclusion

We can separate our logic tests with our UI tests.

Also, we test middleware in isolation.

Data should also be created from scratch for each test.