I am trying to test a simple component.
In my component, I am calling the fetchPdf function with useEffect.
Inside fetchPdf function I am making axios request and if the call was successful I am setting the response (which is the pdf) in the state.
My component:
import React, { useState, useEffect } from "react";
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
// Here I am make an API call - on success I am
// set the pdf from the response into state
// axios.get('url/endpoint')
// .then((res) => {
// if (res.status === 200) {
// setPdf(res.data);
// }
// }).catch((e) => console.log(e.message));
setPdf("Mocked PDF");
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
My test:
import React from "react";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import App from "./App";
Enzyme.configure({ adapter: new Adapter() });
describe("<App />", () => {
let wrapper;
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation((init) => [init, setState]);
beforeEach(() => {
wrapper = Enzyme.shallow(<App />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("calling designPDF", () => {
it("set designPDF into the state", () => {
// Not sure how can I test the fetchPDF function as been called
});
});
});
Codesendbox example
useEffect does not support shallow rendering yet. So you should use Full Rendering API (mount(...)).
Besides, you should mock axios.get method and its resolved value. Call the whenStable function to ensure that the promise returned by the axios.get method is resolved or rejected
E.g.
App.jsx:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
axios
.get('url/endpoint')
.then((res) => {
if (res.status === 200) {
setPdf(res.data);
}
})
.catch((e) => console.log(e.message));
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
App.test.jsx:
import App from './App';
import axios from 'axios';
import { mount } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65310275', () => {
it('should get pdf', async () => {
const getSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: 'Mocked PDF', status: 200 });
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: Mocked PDF');
expect(getSpy).toBeCalledWith('url/endpoint');
getSpy.mockRestore();
});
it('should handle error', async () => {
const mErr = new Error('timeout');
const getSpy = jest.spyOn(axios, 'get').mockRejectedValueOnce(mErr);
const logSpy = jest.spyOn(console, 'log');
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: ');
expect(getSpy).toBeCalledWith('url/endpoint');
expect(logSpy).toBeCalledWith('timeout');
getSpy.mockRestore();
});
});
unit test result:
PASS examples/65310275/App.test.jsx
65310275
✓ should get pdf (40 ms)
✓ should handle error (13 ms)
console.log
timeout
at CustomConsole.<anonymous> (node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866:25)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
App.jsx | 100 | 50 | 100 | 100 | 11
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 6.095 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65310275
Related
I want to simulate clicking a button (#reset), button has onClick fetching with axios. I wan to mock this data, but I'm getting an error: "Cannot read property 'then' of undefined"
Test:
import React from "react";
import PersonsList from "./Components/PersonsList/PersonsList";
import * as axios from "axios";
import { shallow } from "enzyme";
const mockData = [
{
gender: "female",
},
];
jest.mock("axios");
describe("axios", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<PersonsList />);
});
axios.get.mockResolvedValue({ data: mockData });
it("reset", () => {
const resetButton = wrapper.find("#reset");
resetButton.simulate("click");
});
});
button:
<button
className="actions__modify-list"
onClick={() => {
getPeople();
}}
id="reset"
>
Function called on button click:
const getPeople = () => {
const url = "https://randomuser.me/api/?results=10";
axios(url)
.then((res) => {
setPeople(res.data.results);
})
.catch(() => {
setError(true);
});
};
You should mock resolved value for axios(), NOT axios.get() method.
E.g.
PersonsList.jsx:
import axios from 'axios';
import React, { useState } from 'react';
export function PersonsList() {
const [person, setPeople] = useState();
const [error, setError] = useState(false);
const getPeople = () => {
const url = 'https://randomuser.me/api/?results=10';
axios(url)
.then((res) => {
setPeople(res.data.results);
})
.catch(() => {
setError(true);
});
};
return (
<div>
<button
className="actions__modify-list"
onClick={() => {
getPeople();
}}
id="reset"
></button>
</div>
);
}
PersonsList.test.jsx:
import { PersonsList } from './PersonsList';
import axios from 'axios';
import React from 'react';
import { shallow } from 'enzyme';
jest.mock('axios');
const mockData = [{ gender: 'female' }];
describe('68393613', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<PersonsList />);
});
test('should pass', () => {
axios.mockResolvedValue({ data: mockData });
const resetButton = wrapper.find('#reset');
resetButton.simulate('click');
});
});
test result:
PASS examples/68393613/PersonsList.test.jsx (8.311 s)
68393613
✓ should pass (12 ms)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 91.67 | 100 | 80 | 91.67 |
PersonsList.jsx | 91.67 | 100 | 80 | 91.67 | 15
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.837 s, estimated 11 s
Instead of mocking an axios request, I try to test the component using msw, but after the request I don't get the visibility of the content in the component, what am I doing wrong?
My component
import React, {useEffect, useState} from 'react'
import axios from "axios";
export default function TestPage() {
const [testData, setTestData] = useState('')
useEffect(() => {
const getSomeData = async () => {
const data = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
setTestData(data.data.title)
}
getSomeData()
}, [])
return (
<div className='test'>
<h1>{testData}</h1>
</div>
)
}
My test file
import React from 'react'
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import {render, act, screen} from '#testing-library/react'
import '#testing-library/jest-dom/extend-expect'
import TestPage from "../testPage";
const allUsers = [
{title:'User'}
]
const server = setupServer(
rest.get('https://jsonplaceholder.typicode.com/todos/1', async (req, res, ctx) => {
return res(ctx.json( {data: allUsers} ));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers())
afterAll(() => server.close());
test('loads and displays greeting', async () => {
await act(async () => {
await render(<TestPage/>)
})
await screen.findByText('User') //I have no response content here
screen.debug()
})
You do not need to define the data field for ctx.json(), the resolved value of the axios.get() method has a data field. See Response schema
In addition, the data returned by the API is an array.
You don't need to use the act helper function, wait for the result of the API call operation in your test by using one of the async utilities like waitFor or a find* query is enough.
E.g.
TestPage.tsx:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export default function TestPage() {
const [testData, setTestData] = useState('');
useEffect(() => {
const getSomeData = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/todos/1');
console.log(res.data);
setTestData(res.data[0]?.title);
};
getSomeData();
}, []);
return (
<div className="test">
<h1>{testData}</h1>
</div>
);
}
TestPage.test.tsx:
import React from 'react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import TestPage from './TestPage';
const allUsers = [{ title: 'User' }];
const server = setupServer(
rest.get('https://jsonplaceholder.typicode.com/todos/1', async (req, res, ctx) => {
return res(ctx.json(allUsers));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('67902700', () => {
test('loads and displays greeting', async () => {
render(<TestPage />);
await screen.findByText('User');
screen.debug();
});
});
test result:
PASS examples/67902700/TestPage.test.tsx (7.499 s)
67902700
✓ loads and displays greeting (65 ms)
console.log
[ { title: 'User' } ]
at examples/67902700/TestPage.tsx:10:15
console.log
<body>
<div>
<div
class="test"
>
<h1>
User
</h1>
</div>
</div>
</body>
at logDOM (node_modules/#testing-library/dom/dist/pretty-dom.js:82:13)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 7.978 s
To start I'm conditionally rendering a component that's reliant on the useState hook being set as a result of useEffect hook. Here's a code sample:
function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
My current point of contention is that I'm unable to call setResponse or setFail and update the state of fail or response. I believe that I need to use mount as opposed to shallow rendering? Also, I understand that testing philosophies would argue against conducting a test in this fashion. However, I am seeking an a solution that enables updating the state. Any advice would be greatly appreciated.
You can mock fetch API call to and its resolved value. Then, you can assert what exactly the component renders. We should use whenStable function to make sure the mocked API calls are completed.
Component.jsx:
import React, { useState, useEffect } from 'react';
export function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
Component.test.jsx:
import React from 'react';
import { Component } from './Component';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65243384', () => {
let fetch;
beforeEach(() => {
fetch = global.fetch;
});
afterEach(() => {
global.fetch = fetch;
});
it('should success', async () => {
global.fetch = jest.fn().mockResolvedValueOnce('mocked success data');
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('Success:mocked success data');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
it('should fail', async () => {
const mErr = new Error('network');
global.fetch = jest.fn().mockRejectedValueOnce(mErr);
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('ERROR:');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
});
unit test result:
PASS examples/65243384/Component.test.jsx
65243384
✓ should success (44 ms)
✓ should fail (5 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
Component.jsx | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.121 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65243384
import { logout } from '../../actions/user.actions';
const Logout = () => {
useEffect(() => {
Router.push('/any');
}, []);
};
Test File
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks(); // clear all the mocks.
});
it('Should check the route', () => {
const wrapper = mount(<Logout />);
expect(wrapper.find('LoadingMessage')).toBeTruthy();
expect(useDispatch).toBeCalledTimes(1);
const mDispatch = useDispatch();
expect(mDispatch).toBeCalledWith({ type: 'USER_LOGOUT' });
expect(Router.push).toBeCalledWith('/');expect(mDispatch).toBeCalledWith({ type: 'USER_LOGOUT' });
expect(mDispatch).toBeCalledWith('/');
})
})
This is my sample of code that I have written, I want to write a test case for this. So I can test the Router push is happening or not?
Wanting some idea or help.
Thanks in advance.
Below unit test solution mocks useDispatch hook and next/router module without using a mock redux store. Using the mocked redux store and check the state of store after dispatching an action is closer to integration testing.
E.g.
index.jsx:
import React, { useEffect } from 'react';
import Router from 'next/router';
import { useDispatch } from 'react-redux';
import { logout } from './user.actions';
import { LoadingMessage } from './LoadingMessage';
export const Logout = () => {
const dispatch = useDispatch();
const props = {
active: true,
withOverlay: false,
small: false,
};
useEffect(() => {
dispatch(logout());
Router.push('/');
}, []);
return (
<div>
<LoadingMessage {...props}>
<div>Logging out, please wait...</div>
</LoadingMessage>
</div>
);
};
user.actions.ts:
export function logout() {
return {
type: 'LOGOUT',
};
}
LoadingMessage.jsx
export const LoadingMessage = ({ children }) => <div>{children}</div>;
index.test.jsx:
import { Logout } from './';
import { useDispatch } from 'react-redux';
import { mount } from 'enzyme';
import Router from 'next/router';
jest.mock('next/router', () => ({ push: jest.fn() }), { virtual: true });
jest.mock('react-redux', () => {
const originalReactRedux = jest.requireActual('react-redux');
const mDispatch = jest.fn();
const mUseDispatch = jest.fn(() => mDispatch);
return {
...originalReactRedux,
useDispatch: mUseDispatch,
};
});
describe('61928263', () => {
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
it('should pass without using mock store', () => {
const wrapper = mount(<Logout></Logout>);
expect(wrapper.find('LoadingMessage')).toBeTruthy();
expect(useDispatch).toBeCalledTimes(1);
const mDispatch = useDispatch(); // get the mocked dispatch function
expect(mDispatch).toBeCalledWith({ type: 'LOGOUT' });
expect(Router.push).toBeCalledWith('/');
});
});
The outcome for the test:
PASS stackoverflow/61928263/index.test.jsx (8.62s)
61928263
✓ should pass without using mock store (37ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
LoadingMessage.jsx | 100 | 100 | 100 | 100 |
index.jsx | 100 | 100 | 100 | 100 |
user.actions.ts | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.601s
I have a React hooks functional component that I'd like to test with Jest/Enzyme. I would like test its tertiary render behaviour based upon a useState value. I can't seem to find any example online. There is no 'click' to simulate - no API call to mock because at the end, I still need to test based upon the useState value.
In the past, with class components, I could set the state. With the new hooks, I can't.
So, basically - how do I mock an async await inside a mocked submitForm function so that the render behaves properly?
Here's my component:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'core/Form';
export const Parent = ({submitForm}) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = name => evt => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
Here's my testing so far:
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import Parent from './Parent';
import Form from 'core/Form';
let wrapper, props;
.
.
.
describe('<Parent /> rendering', () => {
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Parent {...props} />);
});
afterEach(() => {
jest.clearAllMocks();
});
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(init => [init, setState]);
it('Should render 1 Form', () => {
expect(wrapper.find(Form)).toHaveLength(1);
});
it('renders Redirect after API call', () => {
setRedirect = jest.fn(() => false);
expect(wrapper.find(Redirect)).toHaveLength(1);
});
it('renders Form before API call', () => {
setRedirect = jest.fn(() => true);
expect(wrapper.find(Form)).toHaveLength(1);
});
});
You don't need to spy useState hook. Which means you should not test these hooks and methods of the component directly. Instead, you should test components' behavior(the state, props and what is rendered)
E.g.
index.tsx:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
export const Form = ({ onSubmit, onChange, values }) => <form onSubmit={onSubmit}></form>;
const path = '/user';
export const Parent = ({ submitForm }) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = (name) => (evt) => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
index.test.tsx:
import Parent, { Form } from './';
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
const whenStable = async () =>
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
describe('60137762', () => {
it('should render Form', () => {
const props = { submitForm: jest.fn() };
const wrapper = shallow(<Parent {...props}></Parent>);
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle submit and render Redirect', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(true) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Redirect)).toBeTruthy();
});
it('should handle submit and render Form', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(false) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle error if submit failure', async () => {
const logSpy = jest.spyOn(console, 'log');
const mError = new Error('network');
const props = { submitForm: jest.fn().mockRejectedValueOnce(mError) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(logSpy).toHaveBeenCalledWith('Submit error: ', mError);
});
});
Unit test results with coverage report:
PASS stackoverflow/60137762/index.test.tsx
60137762
✓ should render Form (18ms)
✓ should handle submit and render Redirect (15ms)
✓ should handle submit and render Form (8ms)
✓ should handle error if submit failure (18ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Submit error: Error: network
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:39:20
at step (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:44:23)
at Object.next (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:25:53)
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:19:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:15:12)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:37:47)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 78.57 | 100 | 40 | 93.75 |
index.tsx | 78.57 | 100 | 40 | 93.75 | 12
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 3.716s, estimated 5s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60137762