Roll Your Own Component Generators
Writing quick scripts to generate boilerplate code is a handy technique that can save you tons of time. I recently used this while building a React library that needs a lot of components (a wizard interface), but you can apply this approach to generate any kind of repetitive code structure you need. There are probably way better libraries out there for code generation if you need something more robust, but sometimes writing a simple custom script is all you need. It’s quick to implement, adds no dependencies, and you have complete control over the templates - making it perfect for those times when you need several similar components or files.
First thing to do is create a new file, you could use pretty much any language for this, bash
would be quite good here, but for this we’ll use node
.
# if you dont have a scripts directory already
mkdir -p scripts
# otherwise
touch scripts/generate-component.js
chmod +x scripts/generate-component.js
The actual script itself is fairly self-explanatory, we use Javascripts’ string interpolation to create, well, whatever we want really - anything that we want to end up in a file. I’m sure this technique has some fancy name, problem includes the word templating in there somewhere.
For some React components and tests:
const componentTemplate = (name) => `import React from 'react';
interface ${name}Props {
}
const ${name} = ({}: ${name}Props) => {
return (
<div>
<h1>${name} Component</h1>
</div>
);
};
export default ${name};
`;
const testTemplate = (name) => `import { render, screen } from '@testing-library/react';
import ${name} from './${name}';
describe('${name}', () => {
it('${name} renders successfully', () => {
render(<${name} />);
expect(screen.getByText('${name} Component')).toBeInTheDocument();
});
});
`;
const indexTemplate = (name) => `export { default } from './${name}';
`;
Extend that example as much as you,maybe you want snapshots for every component, maybe every new component you make has a useEffect
or wants some useState
setting up, just add it to your template.
Next the actual function that writes this template to disk - here we take the component name, and a default directory (I often have to split components into libraries, so just make that bit a parameter by default).
We use nodes mkdir
to create a directory, and then join the path with a file name of our choice and the contents of our file.
const createComponent = async (name, directory = 'src/components') => {
try {
const componentDir = join(process.cwd(), directory, name);
await mkdir(componentDir, { recursive: true });
await writeFile(
join(componentDir, `${name}.tsx`),
componentTemplate(name)
);
await writeFile(
join(componentDir, `${name}.test.tsx`),
testTemplate(name)
);
await writeFile(
join(componentDir, 'index.ts'),
indexTemplate(name)
);
console.log(`Created component ${name} in ${componentDir}`);
} catch (error) {
console.error('Error creating component:', error);
}
};
We add a bit of error catching around the script, the whole thing looks something like this:
import { mkdir, writeFile } from 'fs/promises';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const componentTemplate = (name) => `import React from 'react';
interface ${name}Props {
// Define your props here
}
const ${name} = ({}: ${name}Props) => {
return (
<div>
<h1>${name} Component</h1>
</div>
);
};
export default ${name};
`;
const testTemplate = (name) => `import { render, screen } from '@testing-library/react';
import ${name} from './${name}';
describe('${name}', () => {
it('${name} renders successfully', () => {
render(<${name} />);
expect(screen.getByText('${name} Component')).toBeInTheDocument();
});
// Add more tests here
});
`;
const indexTemplate = (name) => `export { default } from './${name}';
`;
const createComponent = async (name, directory = 'src/components') => {
try {
const componentDir = join(process.cwd(), directory, name);
await mkdir(componentDir, { recursive: true });
await writeFile(
join(componentDir, `${name}.tsx`),
componentTemplate(name)
);
await writeFile(
join(componentDir, `${name}.test.tsx`),
testTemplate(name)
);
await writeFile(
join(componentDir, 'index.ts'),
indexTemplate(name)
);
console.log(`Created component ${name} in ${componentDir}`);
} catch (error) {
console.error('Error creating component:', error);
}
};
const args = process.argv.slice(2);
const componentName = args[0];
const directory = args[1];
if (!componentName) {
console.error('Please provide a component name');
process.exit(1);
}
createComponent(componentName, directory);
Lets add this now as a utility in our package.json
{
// ...the rest of the package.json. but dont copy this, because comments aren't allowed in json.
"scripts": {
"generate": "node scripts/generate-component.js"
}
}
Now you can run npm run generate Button <with-a-directory>
and, everything going well, you should have a nicely generated component, with a test and everything 🙃.
To improve on this you could also do things like append the file name to the root index in whereever you are exporting the component from, the worlds your oyster at this point.
If you did, or didn’t enjoy reading this, let me know and i’ll find something more interesting to blog about. Personally I enjoy learning about little tips and tricks like this, I used the time i saved to write this article. Until next time 👨💻