Plop, React Query, JSON Server & Jest

Sofia
8 min readAug 19, 2023

--

A lovely plopfile

What we are about to do is save dozens of really valuable minutes of your life by automating the task of creating new services with React Query. But not only that, we will also save time by creating a test file for the new hook and the resource on a mock server. You may not really need a mock server, but it is really useful in case the backend team has not finished their job yet.

The GitHub repo is at the end of the story.

What is Plop?

In case you already know, you can skip to the steps section. Plop is a small but powerful tool that will save you a lot of time creating good boilerplate. It will also give your team more control when creating certain flows, screens, or services. Plop will create a simple script using a template fully customizable by you, similar to a template from a website but only for the code part.

Steps:

  1. First, create a folder inside your folders project or whatever, and then run this command on the path:
yarn create react-app my-app

In case you do not have yarn installed yet, you can install it by running the following command:

npm install --global yarn

2. Once the process is finished, navigate to the new folder created and then install some dependencies:

yarn add --dev jest

yarn add --dev jest-environment-jsdom

yarn add --dev ts-jest

yarn add --dev plop

yarn add --dev typescript

yarn add axios

yarn add react-router-dom

yarn add react-query

yarn add --dev @testing-library/react-hooks

yarn add --dev @types/react-router-dom

Jest, jest.environment-jsdom, ts-jest and @testing-library/react-hooks are necessary to run our tests.

3. Once everything is installed, go to package.json and edit this line:

"test": "react-scripts test"

to this in order to test your code with Jest:

"test": "jest"

4. Add the next line to “scripts” on package.json

"create-service": "plop service"

5. Then, add this file to the root of your project:

tsconfig.json

{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./src"
},
"include": ["src", "public", "jest.config.js", "babel.config.js"],
"exclude": ["mockServer"]
}

Add this in case you want to avoid semicolons:

prettier.config.js

module.exports = {
singleQuote: true, // Force using single quotes
trailingComma: 'all', // Force the use of a comma at the end of objects
arrowParens: 'avoid', // Force not using parentheses with single parameter functions.
semi: false, //to avoid using semicolons
}

6. Now, please edit your App.js and rename it to App.tsx, then edit the first two lines to:

import './App.css'
const logo = require('./logo.svg') as string

in order to update your code to work with typescript.

7. Add this file to the root of your project:

plopfile.js

const config = plop => {
plop.setGenerator('service', {
description: 'Generate new Service Hoook',
prompts: [
{
type: 'input',
name: 'name',
message: "What's the service domain ?",
},
],
actions: [
{
type: 'add',
path: 'mockServer/data/{{camelCase name}}Data.json',
templateFile: 'plop/templates/services/dataMockServiceTemplate.hbs',
},
{
path: 'mockServer/dbLoader.js',
pattern: /(\/\/ IMPORT JSON FILES)/g,
template:
"const {{camelCase name}}Data = require('./data/{{camelCase name}}Data.json');\n$1",
type: 'modify',
},
{
path: 'mockServer/dbLoader.js',
pattern: /(\/\/ EXPORT JSON FILES)/g,
template: '\t...{{camelCase name}}Data,\n$1',
type: 'modify',
},
{
path: 'src/services/reactQuery/queryKeys.js',
pattern: /(\/\/ ADD NEW KEY HERE)/g,
template: "{{camelCase name}}: '{{camelCase name}}',\n$1",
type: 'modify',
},
{
type: 'add',
path: 'src/services/use{{properCase name}}/use{{properCase name}}.ts',
templateFile: 'plop/templates/services/serviceHookTemplate.hbs',
},
{
type: 'add',
path: 'src/services/use{{properCase name}}/use{{properCase name}}.test.tsx',
templateFile: 'plop/templates/services/serviceTestTSTemplate.hbs',
},
],
})
}

module.exports = config

And add this line to scripts in package.json:

"create-service": "plop service"

In case you are wondering about what this file is about, this file is telling on “prompts” the text the user will see when the script is running. “What’s the service domain?” is the question to the user.

Actions need a type, a path and a template. Types could be add or modify. In case the type is “Add”, we need to specify the path where our new file will be created. In case the type is “Modify”, we need to specify the pattern and the path of the file that will be modified. Patterns could start with things like “// THIS IS OUR PATTERN”.

Templates applies to both cases, template are neccesary to tell plop which code will be modified or created.

8. Add this file on the root of your project:

jest.config.js

const rootDir = '<rootDir>/src'

module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx|js|jsx)$': 'ts-jest',
},
setupFilesAfterEnv: [`${rootDir}/setupTests.ts`],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
require.resolve('./src/tests/file-mock.js'),
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
}

Create a folder named “tests” inside “src” folder and then a file named:

file-mock.js

module.exports = 'test-file-stub'

In case you are wondering why we are adding this file, its because we are using webpack with jest. You can always take a look to the documentation following this link: Using with webpack · Jest (jestjs.io)

9. Now add this file under your src folder. You do not have to copy the optional blocks on this file in case you wont need them:

setupTests.ts

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom

import '@testing-library/jest-dom'

//this is optional, in case you want to mock the window object
global.window.matchMedia = query => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: (_: Event) => true,
})

//this is also optional, in case you want to mock the resize observer
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}))

global.window.HTMLElement.prototype.scrollIntoView = function () {}

//this is optional, in case you want to mock the useNavigate hook
const mockedUsedNavigate = jest.fn()

jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as any),
useNavigate: () => mockedUsedNavigate,
}))

jest.mock('./hooks/useAppContext')

Object.defineProperty(window, 'config', {
value: { API_BASE_URL: 'http://localhost:3004', APP_ENV: 'local' },
})

11. Add the nexts three files under plop/templates:

What is an HBS file?

An HBS file is a template file that is created by Handlebars. Handlebars is a web template system for generating online templates.

serviceTestTemplate.hbs

This the hbs template. Values inside {{ }} will be replaced by plop with the data send by the user.

These are the case modifiers:

  • camelCase: changeFormatToThis
  • snakeCase: change_format_to_this
  • dashCase/kebabCase: change-format-to-this
  • dotCase: change.format.to.this
  • pathCase: change/format/to/this
  • properCase/pascalCase: ChangeFormatToThis
  • lowerCase: change format to this
  • sentenceCase: Change format to this,
  • constantCase: CHANGE_FORMAT_TO_THIS
  • titleCase: Change Format To This

You can find more info at the plop GitHub repo: plopjs/plop: Consistency Made Simple (github.com)

{{ properCase name }} will be replaced using the case modifier “properCase” and the “name” value will be printed in here.

import React from 'react'
import { QueryClientProvider, QueryClient } from 'react-query'
import { renderHook } from '@testing-library/react-hooks'
import { useGet{{properCase name}}DataById } from './use{{properCase name}}'

import INITIAL_APP_STATE from '@storage/state/appState'
import data{{camelCase name}} from '../../../mockServer/data/{{camelCase name}}Data.json'

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})

beforeAll(() => {
jest.mock('axios', () => {
const mAxiosInstance = {
get: jest.fn().mockResolvedValue({
data: data{{camelCase name}},
}),
}
return {
create: jest.fn(() => mAxiosInstance),
}
})
})

test('customHook returns correct data', async () => {
const wrapper = ({ children }: any) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)

const { result, waitFor } = renderHook(
() => useGet{{properCase name}}DataById(INITIAL_APP_STATE, 1),
{ wrapper },
)

await waitFor(() => result.current.isSuccess)

expect(result.current.data).toEqual(data{{camelCase name}}.{{camelCase name}}[0])
})

This block explained:

You can follow this link in case you want to know more about the axios.create() method.

beforeAll(() => { /*before all tells Jest to execute this piece of code 
before all tests */
jest.mock('axios', () => { /* jest.mock() is used to mock a dependencie,
in this case we will be mocking axios */
const mAxiosInstance = { /* we are creating a variable that will contain
our mocked methods from axios like get() and the data returned */
get: jest.fn().mockResolvedValue({
data: data{{camelCase name}},
}),
}
return {
create: jest.fn(() => mAxiosInstance), /* axios.create is essentially a factory to create new instances of Axios */
}
})
})

serviceHookTemplate.hbs

{{ camelCase name }} will be replaced using the case modifier “camelCase” and the “name” value will be printed in here.

import { useQuery } from 'react-query'
import { getAxiosInstance } from 'services/axios'
import { queryKeys } from '../reactQuery/queryKeys'

export async function get{{properCase name}}DataById(
appState: any,
{{camelCase name}}Id: number,
): Promise<Response> {
const { data } = await getAxiosInstance(appState).get(
`/{{camelCase name}}/${ {{camelCase name}}Id }`,
)
return data
}

export function useGet{{properCase name}}DataById(
appState: any,
{{camelCase name}}Id: number,
): any | undefined {
const response = useQuery([queryKeys.{{camelCase name}}, {{camelCase name}}Id], () =>
get{{properCase name}}DataById(appState, {{camelCase name}}Id),
)
return response
}

This block explained:

We are passing to react Query a key and a parameter, in this case the id. Then, we request the data to the mock server by passing the appState and the id requested by the user.

const response  = useQuery([queryKeys.{{camelCase name}}, {{camelCase name}}Id], () =>
get{{properCase name}}DataById(appState, {{camelCase name}}Id),
)

dataMockServiceTemplate.hbs

This is the template used to create the mock server data. You could skip this template in case you do not need this.

{
"{{camelCase name}}" : [
{
"id": 1
}
]
}

Now, the mock server part:

  1. First, create a folder named mockServer on the root of your project, then and the next two files: dbLoader.js
const database = require('./data/database.json')
// IMPORT JSON FILES

module.exports = function () {
return {
...database,
// EXPORT JSON FILES
}
}

server.js

const jsonServer = require('json-server')
const routes = require('./data/routes')
let server = jsonServer.create()
const router = jsonServer.router(require('./dbLoader.js')())
const middlewares = jsonServer.defaults()

server.use(middlewares)

server.use(
jsonServer.rewriter({
...routes,
}),
)
server.use(router)
server.listen(
{
host: 'localhost',
port: 3004,
},
function () {
console.log('JSON Server is running on http://localhost:3004')
},
)

2. Create a folder named “services” inside “src” folder. Inside of “services”, create a folder named reactQuery and then a file named queryKeys.js with this content:

queryKeys.js

export const queryKeys = {
// ADD NEW KEY HERE
}

3. Create a folder named “axios” inside you services folder and add this file:

index.ts

import axios, { AxiosRequestConfig } from 'axios'

export const getAxiosInstance = () => {

const axiosConfig: AxiosRequestConfig = {
baseURL: "",
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
}

return axios.create(axiosConfig)
}

4. Now it’s time to try!

Type on your terminal:

yarn create-component

And voila! Once you finish, plop will generate a custom hook using React Query that is fully functional by using the mock data, and it also can be tested using Jest.

In case you had troubles with something, you can take a look to the Github repo:

solymdev/plop_react_query_hook: Custom hook using React Query and Jest using Plop (github.com)

--

--