In this post, I will introduce how do I setup my development environment for NextJs. My development environments include:
cypress // For component testing
cypress-react-unit-test
storybook // Quickly view react components
tailwindcss // CSS framework
typescript // Provide type safety
In this post, I will assume you have NodeJs and yarn installed in your global environment. Also, I am assuming you know why we need those dependencies and won't explain much about them.
TLDR
Just checkout the repository: https://github.com/cuichenli/nextjs-typescript-cypress-storybook-tailwind
Let's start with NextJs
To start with, let's install create-next-app via yarn so we can use it to setup some basic stuff.
yarn global add create-next-app
create-next-app
After you type the above commands in your terminal, create-next-app will ask you the name of the project, I will use my-app here.
Once you are done with the questions, you can run cd to go into the newly created project.
Typescript
There might be some other typescript templates out there, but I chose to do it myself, as I would like to see what are the things we need to do. So, to start with,
yarn add --dev @types/react typescript @types/node
Next we should convert the generated _app.js and index.js in pages directory to tsx file. For _app.js, simple rename it, for index.js, rename it and remove the imported css (you may need some other configurations to use those import, I will not introduce it here) and simplify the index.js file like this:
import React from "react";
export default function Home() {
return <p>hello</p>;
}
Once the files are renamed, you can run yarn dev to start the NextJs development server. NextJs will kindly help you to generate one simple tsconfig.json and one next-env.d.ts file as it detected you are using TypeScript.
BTW, in this step, I also moved pages into src directory. This is optional of course.
StoryBook
I started with add storybook as global command here, and then use the recommend sb init to initialize the files
yarn global add storybook
yarn sb init
Once the commands finished, you will see two new directories
-
.storybook: This one contains the main configurations for StoryBook, it is created in the root directory. -
stories: This one stores the stories you would like to use. StoryBook will generate a bunch of example stories for you, you can just delete them. In my case, this directory is generated insrcdirectory. And I manually moved it out to root directory.
Configure StoryBook
The generated configurations files .storybook/main.js contains the main configurations for your storybook. In my case, since I moved the generated stories directory, I need to change the entry stories
module.exports = {
// Update stories to where you would like to store them.
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.tsx"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
};
Try StoryBook
I added one simple component to try if the StoryBook is configured correctly.
// src/components/text-input.tsx
import React from "react";
export const TextInputComponent: React.FunctionComponent = () => {
return (
<div>
<input placeholder="type"></input>
</div>
);
};
// ./stories/text-input.stories.tsx
import React from "react";
import { TextInputComponent } from "../src/components/text-input";
export default {
title: "story/text-input",
};
export const Primary: React.FunctionComponent = () => <TextInputComponent />;
Once the above files are added, run
yarn run storybook
and you will see the newly created component.
TailwindCSS
You will soon realize the component is a little bit boring - and it is time to add some style on it to make it a little bit better. So the next step is to configure TailwindCSS.
yarn add tailwindcss
yarn tailwindcss init -p
The above two commands will install tailwind and generate the default configurations for you.
You will see two newly added configuration files - postcss.config.js and tailwind.config.js.
Following the official guidance of tailwind, I added two plugins under postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Note: You must use the interoperable object-based format based on the NextJs document (you can find this note at the bottom).
Then add tailwind into styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Add style to the component
// ./src/components/text-input.tsx
import React from "react";
export const TextInputComponent: React.FunctionComponent = () => {
return (
<div>
<input
placeholder="type"
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
></input>
</div>
);
};
And use it in your main page
// ./src/pages/index.tsx
import { Fragment } from "react";
import { TextInputComponent } from "../components/text-input";
export default function Home() {
return (
<Fragment>
<p>Hello</p>
<TextInputComponent />
</Fragment>
);
}
Then you can start your NextJs server to view it on your main page.
What about StoryBook
Then you start your StoryBook server, and found - the style is not applied. Why? My understanding is that StoryBook does not know we are using tailwind at all - while NextJs knows it because it is imported in the _app.tsx file. So our next step is to let StoryBook be aware of this as well. To achieve this, we should update the .storybook/preview.js file
import "../styles/globals.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
};
Then you start your StoryBook server again, still, you find there is no style (or even worse, you may see error message - I can not recall exactly you will see now.) Basically the reason is StoryBook's webpack component is not using postcss-loader - while NextJs has builtin support for it. To change it, we should update the .storybook/main.js file
module.exports = {
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.tsx"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
webpackFinal: (config) => {
return {
...config,
module: {
...config.module,
rules: [
// Filter out the default css loader
...config.module.rules.filter((rule) => /\.css$/ !== rule.test),
{
test: /\.css/,
use: [
{
loader: "postcss-loader",
options: {
plugins: [require("tailwindcss"), require("autoprefixer")],
postcssOptions: {
ident: "postcss",
sourceMap: true,
},
},
},
],
},
],
},
};
},
};
Note1 We are filtering out one built in css rule here, as the builtin one is not applicable for postcss.
Note2 When speicfy plugins, you can not simply put a string here, it has to be required object.
Once the configurations are placed, you can start your storybook server again and see your styled component.
Cypress and Cypress-React-Unit-Test
To test the newly added component, we can use cypress.
yarn add --dev cypress cypress-react-unit-test
Although in the document of cypress-react-unit-test it says we can initialize cypress with cypress-react-unit-test init command, but I failed to achive it. So I will do it manually. To start, start cypress server once.
yarn cypress open
The above command will initialize your basic cypress configurations - it creates one cypress.json file and one cypress directory.
Then we can follow the document in cypress-react-unit-test to setup it
// cypress.json
{
"experimentalComponentTesting": true
}
// cypress/support/index.js
import "./commands";
require("cypress-react-unit-test/support");
// cypress/tsconfig.json
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts*"],
"compilerOptions": {
"baseUrl": ".",
"jsx": "react",
"types": ["cypress"]
}
}
// ./tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "src" // <- Add this line
},
"include": ["next-env.d.ts", "src"],
"exclude": ["node_modules", ".storybook"]
}
// ./cypress/global.d.ts
/// <reference types="cypress" />
// ./cypress/plugins/index.js
/// <reference types="cypress" />
module.exports = (on, config) => {
return config;
};
Note We need to add the baseUrl line to satisfy cypress, otherwise it will complain (sorry I can not recall what it was complaining about exactly).
After that, you can start to write a simple cypress test
// cypress/component/text-input.spec.tsx
import React from "react";
import { mount } from "cypress-react-unit-test";
import { TextInputComponent } from "../../src/components/text-input";
import "../../styles/globals.css";
describe("HelloWorld component", () => {
it("works", () => {
mount(<TextInputComponent />);
});
});
Then you run yarn cypress open and click the test you just added - however, it wont make it. You will see it is complaining that there is no loader for this file. Guess cypress or cypress-react-unit-test does not have builtin support for Typescript - so we need to add the webpack loader manually. In addition, you also want to set the loader for css, as cypress does not support it by default either.
/// <reference types="cypress" />
const webpackPreprocessor = require("@cypress/webpack-preprocessor");
const commonOptions = require("../../webpack.config");
commonOptions.resolve = {
extensions: [".tsx", ".ts", ".js", ".css"],
};
commonOptions.module.rules.push({
test: /tsx$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-typescript"],
},
},
],
});
commonOptions.module.rules.push({
test: /css$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
],
});
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
const options = {
// send in the options from your webpack.config.js, so it works the same
// as your app's code
webpackOptions: commonOptions,
watchOptions: {},
};
on("file:preprocessor", webpackPreprocessor(options));
return config;
};
Forgive the bad naming, I am really lazy to change it atm.
You may notice that the loader used here is different to the ones we used in StoryBook, because if we use the same configuration as the ones in StoryBook, it wont work. I am still unsure why this is happening.
OK, with all the configuration above, you can now use cypress without any issue.
Main Take Away
WebPack is really important.
Top comments (0)