How to refetch a query when a new subscription arrives in react-apollo - react-apollo

I was wondering if there's an elegant way to trigger the refetch of a query in react-apollo when a subscription receives new data (The data is not important here and will be the same as previous one). I just use subscription here as a notification trigger that tells Query to refetch.
I tried both using Subscription component and subscribeToMore to call "refetch" method in Query's child component but both methods cause infinite re-fetches.
NOTE: I'm using react-apollo v2.1.3 and apollo-client v2.3.5
here's the simplified version of code
<Query
query={GET_QUERY}
variables={{ blah: 'test' }}
>
{({ data, refetch }) => (
<CustomComponent data={data} />
//put subscription here? It'll cause infinite re-rendering/refetch loop
)}
<Query>

Finally I figured it out myself with the inspiration from Pedro's answer.
Thoughts: the problem I'm facing is that I want to call Query's refetch method in Subscription, however, both Query and Subscription components can only be accessed in render method. That is the root cause of infinite refetch/re-rendering. To solve the problem, we need to move the subscription logic out of render method and put it somewhere in a lifecycle method (i.e. componentDidMount) where it won't be called again after a refetch is triggered. Then I decided to use graphql hoc instead of Query component so that I can inject props like refetch, subscribeToMore at the top level of my component, which makes them accessible from any life cycle methods.
Code sample (simplified version):
class CustomComponent extends React.Component {
componentDidMount() {
const { data: { refetch, subscribeToMore }} = this.props;
this.unsubscribe = subscribeToMore({
document: <SUBSCRIBE_GRAPHQL>,
variables: { test: 'blah' },
updateQuery: (prev) => {
refetch();
return prev;
},
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { data: queryResults, loading, error } } = this.props;
if (loading || error) return null;
return <WhatEverYouWant with={queryResults} />
}
}
export default graphql(GET_QUERY)(CustomComponent);

It's possible if you use componentDidMount and componentDidUpdate in the component rendered by the Subscription render props function.
The example uses recompose higher order components to avoid too much boilerplating. Would look something like:
/*
* Component rendered when there's data from subscription
*/
export const SubscriptionHandler = compose(
// This would be the query you want to refetch
graphql(QUERY_GQL, {
name: 'queryName'
}),
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
this.props.queryName.refetch()
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
})
)(UIComponent);
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Another way of accomplishing this while totally separating Query and Subscription components, avoiding loops on re-rendering is using Apollo Automatic Cache updates:
+------------------------------------------+
| |
+----------->| Apollo Store |
| | |
| +------------------------------+-----------+
+ |
client.query |
^ +-----------------+ +---------v-----------+
| | | | |
| | Subscription | | Query |
| | | | |
| | | | +-----------------+ |
| | renderNothing | | | | |
+------------+ | | | Component | |
| | | | | |
| | | +-----------------+ |
| | | |
+-----------------+ +---------------------+
const Component =() => (
<div>
<Subscriber />
<QueryComponent />
</div>
)
/*
* Component that only renders Query data
* updated automatically on query cache updates thanks to
* apollo automatic cache updates
*/
const QueryComponent = graphql(QUERY_GQL, {
name: 'queryName'
})(() => {
return (
<JSX />
);
});
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
/*
* Component rendered when there's data from subscription
*/
const SubscriptionHandler = compose(
// This would be the query you want to refetch
lifecycle({
refetchQuery() {
// condition to refetch based on subscription data received
if (this.props.data) {
var variables = {
...this.props.data // if you need subscription data for the variables
};
// Fetch the query, will automatically update the cache
// and cause QueryComponent re-render
this.client.query(QUERY_GQL, {
variables: {
...variables
}
});
}
},
componentDidMount() {
this.refetchQuery();
},
componentDidUpdate() {
this.refetchQuery();
}
}),
renderNothing
)();
/*
* Component that creates the subscription operation
*/
const Subscriber = ({ username }) => {
return (
<Subscription
subscription={SUBSCRIPTION_GQL}
variables={{ ...variables }}
>
{({ data, loading, error }) => {
if (loading || error) {
return null;
}
return <SubscriptionHandler data={data} />;
}}
</Subscription>
);
});
Note:
compose and lifecycle are recompose methods that enable easier a cleaner higher order composition.

Related

How to test Vue3 and intertia with jest

In a Laravel + Vue3 + Inertia project which setup using Laravel Mix, how we can create front-end tests?
Especially, I have no idea how to handle Inertia's Share Data, usePage() and useForm methods?
The first error I'm facing is:
TypeError: Cannot read properties of undefined (reading 'someSharedData')
2 |
3 | export const handleSometing = (something) =>
> 4 | usePage().props.value.someSharedData
| ^
5 | ...
6 | )
After googling some useless hours and finding nothing to this exact problem, I've found this solution.
The key was in Jest Partial Mocking!
You can mock useForm, usePage, and then Shared Data using Jest Partial Mocking.
After setup the vue-test-util, I have created this test file and it was working like a charm.
In the below example, the i18n is mocked using the config object of the vue-test-utils.
The Inertia's methods are mocked by jest.mock().
import { config, shallowMount } from '#vue/test-utils'
import Dashboard from '#/Components/ExampleComponent'
config.global.mocks = {
$t: () => '',
}
jest.mock('#inertiajs/inertia-vue3', () => ({
__esModule: true,
...jest.requireActual('#inertiajs/inertia-vue3'), // Keep the rest of Inertia untouched!
useForm: () => ({
/** Return what you need **/
/** Don't forget to mock post, put, ... methods **/
}),
usePage: () => ({
props: {
value: {
someSharedData: 'something',
},
},
}),
}))
test('Render ExampleComponent without crash', () => {
const wrapper = shallowMount(ExampleComponent, {
props: {
otherPageProps: {}
}
})
expect(wrapper.text()).toContain('Hi! I am ExampleComponent.')
})

how to jest test connected component which has api calls in componentDidMount

I am trying to test a connected component but it does not seem to make the api calls in the componentDidMount function. I need it to make the api calls so i can test the how this component woudl render depending on the values returned from api calls. api calls are made by axios using redux actions. everything stored in redux.
here is my test
it('should dispatch an action on mount', () => {
const component = shallow(
<ProcessingStatus store={store}/>
);
const didMount = jest.spyOn(component, 'componentDidMount');
expect(didMount).toHaveBeenCalledTimes(1);
//console.log(component.html())
expect(store.dispatch).toHaveBeenCalledTimes(3);
});
this is the componentDidMount in my component
componentDidMount() {
const {
matches: { params: { id } },
processingStatus,
securityStatus,
routingStatus,
soxStatus,
remedyStatus,
user: {
priv: {
my_sox_requests,
view_remedy
}
}
} = this.props;
let params = 'id=' + id;
if(processingStatus !== undefined){
processingStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(securityStatus !== undefined){
securityStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(routingStatus !== undefined){
routingStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(my_sox_requests && my_sox_requests === 'on' && soxStatus !== undefined){
soxStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
if(view_remedy && view_remedy === 'on' && remedyStatus !== undefined){
remedyStatus(params)
.catch(thrown => {
console.log(thrown);
});
}
}
Error i get is
FAIL tests/jest/components/common/ProcessingStatus/index.test.js
<ProcessingStatus />
✓ should render with given state from Redux store (90ms)
✕ should dispatch an action on mount (7ms)
● <ProcessingStatus /> › should dispatch an action on mount
Cannot spy the componentDidMount property because it is not a function; undefined given instead
85 | <ProcessingStatus store={store}/>
86 | );
> 87 | const didMount = jest.spyOn(component, 'componentDidMount');
| ^
88 | expect(didMount).toHaveBeenCalledTimes(1);
89 |
90 | //console.log(component.html())
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:841:15)
at Object.<anonymous> (tests/jest/components/common/ProcessingStatus/index.test.js:87:31)
I tried with const didMount = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount'); and error i get is
● <ProcessingStatus /> › should dispatch an action on mount
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
85 | );
86 | const didMount = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount');
> 87 | expect(didMount).toHaveBeenCalledTimes(1);
| ^
88 |
89 | //console.log(component.html())
90 | expect(store.dispatch).toHaveBeenCalledTimes(3);
I managed to test didmount being called but not sure how to check if the api calls have been made.
it('should run componentDidMount', () => {
spy = jest.spyOn(ProcessingStatus.prototype, 'componentDidMount');
component = mount(
<ProcessingStatus store={store}/>
);
expect(spy).toHaveBeenCalledTimes(1);
});
I've been searching a similar question here and found out that you have some order errors:
you should set the spyOn componentDidMount before the shallow(Component)
you should ask for Component.prototype.componentDidMount was called after shallow component
jest.spyOn(Component.prototype, 'componentDidMount')
shallow(<Component/>)
expect(Component.prototype.componentDidMount).toHaveBeenCalled();
if the component expect to receive props functions that are going to be called inside the componentDidMount, you should add them when shallow Component like
const mockExpectedFunction= jest.fn()
shallow(<Component expectedFunction={mockExpectedFunction} />

pass parameter in URL using vuejs and laravel

I wanna display data with specefic ID in laravel using vuejs.
I get the ID from the link but it seems that there is no request sent to the controller.
api.php :
<?php
use Illuminate\Http\Request;
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Route::resource('user','API\UserController');
Route::resource('departement','API\DepartementController');
Route::resource('specialite','API\SpecialiteController')->parameters(['specialite'=>'id']);
my controller :
public function show($id)
{
$specialite=Specialite::with('dep')->findOrFail($id);
$spec = Specialite::with('dep')->where('id',$specialite)->get();
return $spec;
}
my view :
<script>
export default {
data(){
return{
specialites:{},
form: new Form({
id:'',
name:'',
user_id:'',
bio:''
}),
id:0,
}
},
methods: {
loadspecialite(){
//axios.get('api/user').then(({data})=>(this.enseignants=data.data));
axios.get('api/specialite/'+this.id).then(response=>{this.specialites=response.data;});
},
created() {
this.id=this.$route.params.id;
this.loadspecialite();
Fire.$on('AfterCreate',()=>{
this.loadspecialite();
})
}
}
</script>
Vue-router:
let routes = [
{ path: '/Profile/:id', component: require('./components/a.vue').default },
]
thank you.
hope tou will help me.
Firstly, I don't see how this.id would carry the id from the router as created is not guaranteed to have been fired AFTER the router has routed.
Your loadspecialite should get the value from the currentRoute when called and i think the var is slightly wrong:
let id = this.$router.currentRoute.params.id;
Your route resource should be:
Route::resource('specialite','API\SpecialiteController');
The request uri would be:
axios.get(`/api/specialite/${id}`).then(...)
You can find out the exact uri path for all registered routes in Laravel by using an SSH terminal to run console command: php artisan route:list
This should produce the following:
+--------+-----------+----------------------------------+------------------------+------------------------------------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+----------------------------------+------------------------+------------------------------------------------------------------------+--------------+
| | GET|HEAD | api/specialite | api.specialite.index | App\Http\Controllers\API\ApplicationController#index | api,auth:api |
| | POST | api/specialite | api.specialite.store | App\Http\Controllers\API\ApplicationController#store | api,auth:api |
| | GET|HEAD | api/specialite/create | api.specialite.create | App\Http\Controllers\API\ApplicationController#create | api,auth:api |
| | GET|HEAD | api/specialite/{specialite} | api.specialite.show | App\Http\Controllers\API\ApplicationController#show | api,auth:api |
| | PUT|PATCH | api/specialite/{specialite} | api.specialite.update | App\Http\Controllers\API\ApplicationController#update | api,auth:api |
| | DELETE | api/specialite/{specialite} | api.specialite.destroy | App\Http\Controllers\API\ApplicationController#destroy | api,auth:api |
| | GET|HEAD | api/specialite/{specialite}/edit | api.specialite.edit | App\Http\Controllers\API\ApplicationController#edit | api,auth:api |
P.S. there is no need to create a form object if you are not sending any attached files, Laravel and axios will revert to use JSON by default with ajax requests.
Laravel will return JSON object by default in response to a JSON ajax call direct from a resource on your controller:
function show($id) {
return Specialite::findOrFail($id);
}
Fail will return a 400+ header that in turn can be handled by axsios .catch
.catch( error => { console.log(error.response.message) } )
Laravel from validation messages would be accessible via:
.catch( error => { console.log(error.response.data.errors) } )
Axios will post an object/array as a JSON request:
data() {
return {
form: {
id:'',
name:'',
user_id:'',
bio:''
},
}
}
...
axios.post('/api/specialite',this.form).then(...);
I do believe that the code is functioning fine. It is a formatting error in the vue component object. Basically your created() handler is in the due methods, thus it won't be handled when the created event is done.
// your code snippet where there is an issue
methods: {
loadspecialite(){
//axios.get('api/user').then(({data})=>(this.enseignants=data.data));
axios.get('api/specialite/'+this.id).then(response=>{this.specialites=response.data;});
}, // end of loadspecialite
created() {
this.id=this.$route.params.id;
this.loadspecialite();
Fire.$on('AfterCreate',()=>{
this.loadspecialite();
})
} // end of created
} //end of methods
What you should do is just remove the created() out of methods and also check the syntax of the function again.
const Foo = {
template: '<div>foo</div>'
}
const Bar = {
template: '<div><span> got {{form}}</span></div>',
data() {
return {
specialites: {},
form: 'fetching...',
id: 0,
}
},
methods: {
loadspecialite() {
// test method for getting some data
axios.get('https://httpbin.org/anything/' + this.id)
.then(response => {
this.form = response.data.url;
}).catch(error => {
console.error(error)
})
},
}, // <- this is the end of methods {}
/**
* Created method outside of methods scope
*/
created() {
this.id = this.$route.params.id;
this.loadspecialite();
}
}
// rest is vues demo router stuff
const routes = [{
path: '/foo',
component: Foo
},
{
path: '/bar/:id',
component: Bar
}
]
const router = new VueRouter({
routes // short for `routes: routes`
})
const app = new Vue({
router
}).$mount('#app')
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue Routed</title>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<style>
button {
padding: 0.75rem;
background: #eee;
border: 1px solid #eaeaea;
cursor: pointer;
color: #000
}
button:active {
color: #000;
box-shadow: 0px 2px 6px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div id="app">
<h1>Hello App!</h1>
<p>
<span> Click a button </span>
<router-link to="/foo"><button>Go to Foo</button></router-link>
<router-link to="/bar/3"><button>Go to Where it will get the data</button></router-link>
</p>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view></router-view>
</div>
</body>
</html>
All thing has set well, just your show method should respond in JSON:
use Illuminate\Http\Response;
function show($id) {
result = Specialite::findOrFail($id);
return response()->json($result,Response::HTTP_OK);
}

What are the rules around the re-rendering of the Apollo Query component?

I have a GraphQL field with the following definition:
query GetData($date: DateTime, $id: Int) {
getData(date: $date, id: $id) {
...Data
}
}
I have been consuming this data in React using the Apollo Client Query component, like so:
ids.map((id) => (
<Query
key={id}
query={Queries.getData}
variables={{
date: date,
id: id,
}}
fetchPolicy="network-only"
>
{({
loading,
error,
data,
}: QueryResult<{ data: any}>) => {
return (
);
}}
</Query>
In a separate component, I have need to reuse this data (for all ids) in the Apollo cache. For this, I wrote a local resolver
resolvers: {
Query: {
getAllData: (parent: any, args: any, cache: any) => {
let count = 0;
args.ids.forEach((id: number) => {
try {
const cacheId = `TypeName:${id}`;
const locationSchedule = client.readFragment({
id: cacheId,
fragment: Fragments.Fragment,
fragmentName: Fragments.Fragment.definitions[0].name.value,
});
// count stuff and store in count variable
} catch (e) {
console.log(e);
}
});
return { count, __typename: 'Test' };
},
},
This is invoked from my other component like so:
<Query
query={Queries.getAllData}
variables={{
date: date,
ids: ids,
}}
//pollInterval={500}
>
{({
loading,
error,
data,
}: QueryResult<{ getAllData: { count: number } }>) => {
const count=
data && data.getAllData && data.getAllData.count;
return (
<li className="actions__group">
<div className="actions__item">
<Button
className={'button button--fill-green'}
onClick={this.onAction}
children={
count? (
<span className="button__badge">
{count}
</span>
) : (
undefined
)
}
disabled={count=== 0}
/>
</div>
</li>
);
}}
</Query>
The problem that I have is that the second Query component (the one with the getAllData query) does not re-render when the cache data is mutated (the first component updates fine). Everything seems to be wired up correctly (when I turn on polling the second component re-renders correctly).
What are the rules around when a Query component re-renders? I can't see why the first component re-renders when the second one does not.

mocking a promise with shallow rendering using jest in react redux app

I have looked at the following tutorials https://hackernoon.com/unit-testing-redux-connected-components-692fa3c4441c https://airbnb.io/enzyme/docs/api/shallow.html and tried to create a shallow rendered test of a component but i have actions being triggered on render which collect data and help render the component. how can i mock this?
tests/jest/containers/homecontent.js
import configureStore from 'redux-mock-store'
import { shallow } from 'enzyme';
import { HomeContent } from '../../../app/containers/home';
const passMetaBack = meta => {
this.setState({
title: 'test',
description: 'test'
});
};
// create any initial state needed
const initialState = {};
// here it is possible to pass in any middleware if needed into //configureStore
const mockStore = configureStore();
describe('Login Component', () => {
let wrapper;
let store;
beforeEach(() => {
// our mock login function to replace the one provided by mapDispatchToProps
const mockLoginfn = jest.fn();
//creates the store with any initial state or middleware needed
store = mockStore(initialState)
wrapper = shallow(<HomeContent isGuest={false} isReady={true} priv={{}} passMetaBack={passMetaBack} fetchContents={mockLoginfn} />)
});
it('+++ render the DUMB component', () => {
expect(wrapper.length).toEqual(1)
});
});
The error i get is
FAIL tests/jest/containers/homecontent.test.js
Login Component
✕ +++ render the DUMB component (25ms)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'then' of undefined
38 | if(this.props.isReady && this.props.priv != undefined){
39 | let self = this;
> 40 | this.props.fetchContents()
41 | .then(response => {
42 | let data = response.payload.data;
43 | if (data.header.error) {
at HomeContent.initData (app/containers/home.js:40:7)
at HomeContent.render (app/containers/home.js:71:12)
at ReactShallowRenderer._mountClassComponent (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:195:37)
at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:143:14)
at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:287:35
at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:103:16)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:286:68)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:119:22)
at shallow (node_modules/enzyme/build/shallow.js:19:10)
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:24:19)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'length' of undefined
26 |
27 | it('+++ render the DUMB component', () => {
> 28 | expect(wrapper.length).toEqual(1)
29 | });
30 | });
31 |
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:28:24)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.218s
Ran all test suites matching /tests\/jest\/containers\/homecontent.test.js/i.
this.props.fetchContents() comes in from an action on the component
mockLoginfn is used as this.props.fetchContents in the component. fetchContents is a function that returns a promise, whereas mockLoginfn is a jest mock function that doesn't return anything.
So, you need to provide a mock implementation for the mockLoginfn so it behaves like a promise. For example (using the code snippet above):
const mockLoginfn = jest.fn();
mockLoginfn.mockImplementation(() => Promise.resolve({
payload: {
data: {
header: {
error: 'some error'
}
}
}
}));

Resources