How to make good React components

Architecture

Folder toto

  • toto.component.js
  • toto.container.js
  • toto.style.(css|js)
  • toto.test.js
  • index.js

Component File

  • Must be under 200 lines long
  • No function declared in the render
  • Type :
    • Component
    • PureComponent
    • Functional

Component File

import React from 'react';
import PropTypes from 'prop-types';
import styles from './menu-link.style.css';

export default class MenuLink extends React.PureComponent {

  render() {
    return (
      <div className={styles.menuLink} onClick={this.props.next}>
        <span className={styles.label}>
          {this.props.label}
        </span>
      </div>
    );
  }
}

MenuLink.defaultProps = {
  label: '-',
  next: () => {},
};

MenuLink.propTypes = {
  alertStatus: PropTypes.numb.isRequired,
  next: PropTypes.func.isRequired,
  label: PropTypes.string.isRequired,
};

Style File

  • Belongs to the component
  • Agnostic

Container File

  • No render function
  • Provides data and behavior
    to the presentational component

Container File

import { connect } from 'react-redux';
import MenuLink from './menu-link.component';
import { next } from '../actions/navigate';

function mapStateToProps(state) {
  return {
    alertStatus: state.alertStatus
  };
}

function mapDispatchToProps() {
  return {
    next: () => dispatch(next()),
  };
}

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

Test File

  • Contains Unit tests (functions)
  • Contains Snapshot tests

Test file

import React from 'react';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';

import MenuLink from './menu-link.component';

describe('[Unit] MenuLink', () => {
  it('Should call the "next" function', () => {
    const nextMock = jest.fn();
    container.setProps({ alertStatus: 1, label: 'Link !', next: nextMock });
    container.simulate('click');
    expect(nextMock.mock.calls.length).toEqual(1);
  });
});

describe('[Snapshot] MenuLink', () => {
  const data = [
    {
      props: { alertStatus: 1, label: 'A sweet button', next: () => {} },
      label: 'default',
    },
  ];

  data.forEach(({ props, label }) => {
    const container = renderer.create(<MenuLink {...props} />);
    it(`should match snapshot : ${label}`, () => {
      expect(container.toJSON()).toMatchSnapshot();
    });
  });
});

Extract Containers from coverage

"jest": {
    "rootDir": ".",
    "roots": [
      "<rootDir>/client/source",
    ],
    "moduleNameMapper": {
      "^.+\\.(css|less)$": "<rootDir>/client/source/CSSStub.js",
      "^.+\\.(jpg|png)$": "<rootDir>/client/source/imgStub.js",
      "^.+\\.(svg)$": "<rootDir>/client/source/svgStub.js"      
    },
    "collectCoverage": true,
    "coverageDirectory": "<rootDir>/coverage",
    "collectCoverageFrom": [
      "**/*.{js,jsx}",
      "!**/*.container.{js,jsx}"
    ],
    "coveragePathIgnorePatterns": [
      "<rootDir>/client/source/actions/",
      "<rootDir>/node_modules/"
    ]
  },

Refactoring

  • Create the folder and files
  • Write or rework tests
  • Extract connections
  • Extract styles
Made with Slides.com