How I structure my components
__snapshots__/
index.jsx
index.test.jsx
Component.mdx
styles.module.scss
// For a library there's also
component.spec.js
styles.theme1.scss
styles.theme2.scss
Breaking it down
snapshots
A folder for my snapshot tests, it contains snapshot files created by Jest.
index.jsx
The actual component code, the file is always called index.jsx
because it makes the import smaller – I can import it using the directory rather than the file itself.
Components will always be functional, this example is oversimplified but anything requiring state or lifecycle events will use hooks. I don’t routinely need to write class based components anymore.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './styles.module.scss';
function Key (props) {
const {
children, className,
} = props;
const classes = classNames('u-kbd', styles.key, className);
return (
<div
className={classes}
>
{ children }
</div>
);
}
Next up is the typechecking, I use PropTypes to assign a type to every prop. Unless a prop isRequired it will also have a corresponding default prop.
Key.propTypes = {
/**
* This element can have children
*/
children: PropTypes.node.isRequired,
/**
* Additional classes
*/
className: PropTypes.string,
};
Key.defaultProps = {
className: null,
};
The final part of the file is the export. For more complex components this will also be where the wrappings are (higher order components like Redux etc).
export default Key;
index.test.jsx
This is the unit test file, I use Jest with react testing library.
At the top of every test file I have the same should be defined
test, this acts as a smoke test – if it fails then it signifies bigger problems with the code.
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import Key from './index';
describe('Key', () => {
afterEach(cleanup);
it('should be defined', () => {
expect(Key).toBeDefined();
});
});
Next is the snapshot test, it compares the code output to a stored version (created when the test is first ran).
it('renders correctly', () => {
const { asFragment } = render(
<Key>
key
</Key>,
);
expect(asFragment()).toMatchSnapshot();
});
});
Component.mdx
Documentation file. I use docz as a living styleguide, every variation of the component (size, colour etc) is documented here.
The mdx file is a mix of markdown and jsx, the <Playground>
component is a live editable playground that renders its content in the docs.
At the end of the file is the <PropsTable>
which uses the comments from the PropTypes to render a table of props.
---
name: Key
route: /typography/key
menu: Typography
---
import { Playground, PropsTable } from 'docz'
import Key from './index';
# Key
This styles a keyboard key. It can be used inline (ie it's not a block level element).
## Basic usage
<Playground>
<Key>
key
</Key>
</Playground>
## Properties
<PropsTable of={Key} />
styles.module.scss
This contains the styles that are specific to this component. More general styles are kept in regular global stylesheets.
I use CSS Modules for most styling, this means my class names are scoped locally and guaranteed to be unique. By doing this I don’t need to use BEM or SMACSS or another global CSS convention.
As a .scss
file I can continue to use imports, nesting, and all the features of sass.
.key {
border: 1px solid #ccc;
font-family: monospace;
}
For libraries
If I’m writing a React library then I’ve got a few more files.
component.spec.js
I use Cypress for automated end-to-end testing, it simulates user responses and executes its tests in a browser.
I usually keep Cypress tests in a dedicated folder but for libraries I keep them with the components they test, it just makes it a little easier to keep track.
styles.theme1.scss
If my library has themes, then theme styling lives with the components. I use node-sass-glob-importer to find all the theme files and pull them together.
These files aren’t modules, the code here follows the regular CSS rules of specificity.
.theme {
@import '**/*.theme1.scss';
}