React is awesome, It gives you the power to create truly amazing apps that are performant and fast. But that's not all there is to building an app, is it? Having built multiple large apps based on React, I have discovered that the workflow is as important as the output. With a great workflow, maintenance will be easy, errors will be less and debugging will be a cinch.
So how can we get the best out of this amazing library? By using tools to optimize our workflow of course. The tools in question are Flow (For static typing), EsLint (For adhering to good coding patterns), Jest and Enzyme (For Testing).
Flow
Javascript is a dynamically typed language and so is React (goes without saying). This dynamism though convenient introduces a lot of problems with error detection and hence debugging. Statically typed languages evaluates data types at compile time, in most cases you even see the errors in your code editor before running them while dynamically typed languages on the other hand only evaluates at runtime, which means you detect type errors after the program has tried to run.
Take a look at the code snippet below
const myObj = 2;
// A lenghty stretch of code later...
myObj();
The code snippet above will result in an error, specifically "TypeError: myObj is not a function". If this were a statically typed language, you would detected this bug earlier and fixed it before even running. Though this is an oversimplified version of what could happen, this small issue can sometimes cost you a lot of time. For example, if this piece of code would not run until later on in the program, it could easily sneak past the developer's initial test and cause issues later.
To solve this issue we make use of static type checkers, in this case Flow (https://flow.org/en/). Flow is a static type checker for Javascript which means it checks your types at compile time just like other statically typed languages.
Incorporating Flow into your workflow can be tedious at first and it actually has a bit of a learning curve, but trust me the benefits far outweigh the extra effort.
Applying flow to the code snippet above
// @flow
const myObj = 2;
// A lenghty stretch of code later...
myObj();
Flow will catch this error and display the information in your code editor or it can also catch the error when you run the flow command in your cli. Here's a sample of what flow will output in your editor
As you can see, flow tells you its not a function and even gives you further information on what type it is. Using flow will help
You code faster and with confidence (Since you don't need to run your code before seeing these type bugs).
Understand your code even better
Work better in teams (Interpretation is way easier and your code base is easier to understand).
Intellisense is better
esLint
The importance of Linting cannot be stressed enough. Linting is a code analysis tool and is part of the white box testing process. While unit tests will test your output and general program behaviour, Linting analyses the internal structure of your code.
What is Linting ? Linting is the process of checking your code for logical and stylistic errors. Linters make sure you adhere to a coding standard, provides consistency and shows you possible logical errors. A linter is a program that performs this analyses on your code by going through it. Using a Linter in a team can make the codebase look like it was written by just one person.
There are several Linters out there but my most preferred is esLint because of the robust set of rules it has and also because it's very flexible and easily configurable. You can even write your own rules that your codebase must adhere to.
Jest and Enzyme
Writing unit tests for your app is an extremely important exercise and luckily we have Jest and Enzyme to make this process really easy (Thank you facebook, Thank you airbnb).
Despite the importance of unit testing in React apps, I have seen a lot of people not bother with this which I must say is a mistake. Jest and Enzyme provide awesome testing tools such as Shallow rendering (Rendering only the component without its children for testing), Snapshot testing (Rendered output of your component stored on file and compared against to ensure your component doesn't change) and code coverage out of the box.
Testing a React component can be as simple as
it('render <ComponentX /> without errors', () => {
const wrapper = shallow(<ComponentX />);
expect(wrapper).toMatchSnapshot();
});
// or with a function spy
it('call function on button click', () => {
const funcToCall = jest.fn();
const wrapper = shallow(<ComponentX callFunc={funcToCall}/>);
const btn = wrapper.find('button');
btn.simulate('click');
expect(funcToCall).toHaveBeenCalled();
});
Of course the test could become more complex depending on what you wish to test, but you get the general idea. Jest is the test framework itself that has a task runner, assertion framework and good mocking support. Enzyme on the other hand is a library that provides an easier interface to write unit tests.
All together now
Create React App
For this article I will make use of CRA (Create React App) , the easiest way to start a React app. Grab yourself a copy by running
npx create-react-app <your app name >
Enter the folder through your cli to install the rest of the tools.
Flow
Flow configuration comes with your CRA, but you need to install flow-bin into your workspace in order to use it (Read more about Flow bin).
To install Flow follow these steps:
- Run npm install --D flow-bin to install flow-bin.
- Run ./node_modules/.bin/flow init to create a new .flowconfig file
- Add "flow": "flow" to the scripts section of your package.json.
- Run ./node_modules/.bin/flow to see if it works. You should get a No Errors response. Note: To make things easier, you should install flow globally by running npm i -g flow-bin. Once that is done you don't need ./node_modules/.bin/flow any longer, you can just run "flow" from your cli.
- The No errors! message comes up because you have not started flow typing any files. To see flow in action add // @flow at the top of any of your js or jsx files and run flow again. You will get messages detailing the errors and the file they exist in.
esLint
To get started with esLint do the following
- Run npm i -D eslint to install esLint.
- Once the installation is done, run the following command ./node_modules/.bin/eslint --init. (NB: Again you can install eslint globally by running npm i -g eslint). The init command will ask you about the linting rules you wish to use. Would you like to create yours or would you like to use a popular coding style
A popular choice and the one I usually use is the airbnb style. You also get questions about if you use React (Obviously) and which configuration file type would you like to use (JSON, Javascript or YAML), I mostly use javascript. Then finally you will be asked to install eslint's dependencies, install them to finalise.
- Once done with the config an eslintrc.js will be generated for you (the file extension will depend on config file type you chose). You need to copy the following command into the .eslintrc.js file
// original file
module.exports = {
"extends": "airbnb"
};
// change to this
module.exports = {
"extends": ["airbnb", "plugin:flowtype/recommended"],
"env": {
"jest": true
},
"parser": "babel-eslint",
"plugins": [
"flowtype"
],
};
We are almost done, just one more step.
Jest and Enzyme
Again the good people behind CRA included Jest as a default test runner (Read more), but it doesn't come with enzyme installed. To install enzyme run the following command
npm install --save-dev enzyme enzyme-adapter-react-16 enzyme-to-json
Then update your jest config in package.json by adding
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
}
Next we need to create configurations for enzyme to work with react 16. Create a file called setupTests.js in the src folder, so that we ./src/setupTests.js exists. CRA will find this file by itself, but if you are not making use of CRA, update your jest config in package.json by adding "setupFiles": ["./src/setupTests.js"] to it. Add the following command to setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
Now we are all set. If everything went well, you should already see eslint making corrections to your code with red underline.
Sample program
Let's write a simple program that will be flow typed and unit tested.
Say I have these components
// App.jsx
import React, { Component } from 'react';
import './App.css';
import MyButton from './components/MyButton';
class App extends Component {
constructor() {
super();
this.state = {
count: 1,
};
this.countFunc = this.countFunc.bind(this);
}
countFunc() {
this.setState({
count,
});
}
render() {
const { count } = this.state;
return (
<div>
<h1>{count + 1}</h1>
<MyButton name="Click Me" countFunc={this.countFunc} />
</div>
);
}
}
export default App;
And...
//MyButton.jsx
import React from 'react';
const MyButton = ({ name, countFunc }) => (
<button type="button" onClick={() => countFunc(2)}>{name}</button>
);
export default MyButton;
As it is, they are both just regular functions with no flow typing. The button is passing a number back to the App component but if for some reason that changes the program breaks or loosing meaning (logical error).
// @flow
import React, { Component } from 'react';
import './App.css';
import MyButton from './components/MyButton';
type State = {
count: number,
}
type Props = {}
class App extends Component<Props, State> {
constructor() {
super();
this.state = {
count: 1,
};
this.countFunc = this.countFunc.bind(this);
}
countFunc: (count: number)=>void
countFunc(count: number) {
this.setState({
count,
});
}
render() {
const { count } = this.state;
return (
<div>
<h1>{count + 1}</h1>
<MyButton name="Click Me" countFunc={this.countFunc} />
</div>
);
}
}
export default App;
And ...
// @flow
import React from 'react';
type Props = {
name: string,
countFunc: (count: number) => void
};
const MyButton = ({ name, countFunc }: Props) => (
<button type="button" onClick={() => countFunc(2)}>{name}</button>
);
export default MyButton;
This is way more readable and we are sure to get a warning if the type changes.
Now for the tests
// Very simple test to check if App Renders
import React from 'react';
import { shallow } from 'enzyme';
import App from './App';
describe('<MyButton />', () => {
it('renders without crashing', () => {
const wrapper = shallow(<App />);
expect(wrapper.length).toEqual(1);
});
});
And ...
import React from 'react';
import { shallow } from 'enzyme';
import MyButton from './MyButton';
describe('<MyButton />', () => {
it('Should render without crashing', () => {
const wrapper = shallow(<MyButton />);
expect(wrapper.length).toEqual(1);
});
it('Should render without crashing', () => {
const mockCountFunc = jest.fn();
const wrapper = shallow(<MyButton countFunc={mockCountFunc} />);
const btn = wrapper.find('button');
btn.simulate('click');
expect(mockCountFunc).toHaveBeenCalled();
});
});
The above test for MyButton just tests if MyButton renders successfully and also tests if when the button is clicked it will call the countFunc prop being passed to it.
You can find the complete code here Code Sample
Conclusion
If like me you make use of Vscode, there is an extra step to take to ensure everything works smoothly. You need to make eslint allow you define flow types. If you have setup this project on your own, you might have come across an error stating only .ts files can define types (or something like that). To make this error disappear, open your settings (on Mac that will be clicking the code menu and going to preferences settings and switching to workspace settings). Open the workspace settings and add this setting
"javascript.validate.enable":false
And you are set.
Lastly, the whole process might be a lot to take in and can be overwhelming, but you will get used to it. As a general rule of thumb, I follow this pattern. I write my tests fail and let them fail, I write my Flow types next and then I write my component. Then I adjust the component to fit the previous two.
Happy coding and leave your comments. :-)
Top comments (4)
Thank you for the post! I had one issue after enabling eslint as instructed, the spread operator (...) cause syntax error when React compiles the code. Do you happen to have a similar issue?
Hi Ken,
Sorry my reply took some time. Took my vacation time early this year. I'm not sure i follow though on where you made use of the spread operator that caused the issue. Care to explain ?
Please do not worries about the issue since it actually was a side problem of a different syntax error. Thanks for getting back!
Great,glad to see you solved it.cheers