I have a set of functions in a util module. Most of the functions are simple data manipulation, however, one of them makes a REST call. Many modules/classes call this method that makes the REST call for its values. Instead of the app/browser making multiple (>20) of the calls, can i make 1, and save the result. Then return the saved result and refrain from making subsequent calls?
utils.ts
import axios from 'axios'
import { of, from } from 'rxjs';
import { shareReplay, switchMap, first } from 'rxjs/operators';
let cache: Observable<any>; // <-- global cache to set/get
export function makeRequest() {
const promise = axios.get('http:rest-server:1234/user/data');
const obs = from(promise);
return obs.pipe(
map(res => { return of(res) })
catchError(err => { cache })
);
}
export function getUserData() {
if(!cache) { // <-- if cache isnt empty, make the REST call
cache = makeRequest()
.pipe(
shareReplay(2),
first()
)
}
return cache; // <- return the originally populated cache
}
index.ts
import * as Config from './utils';
myComponent.ts
import { Config } from 'my-helper-library';
export class MyComponent {
public init() {
Config.getUserData()
.subscribe(.......
}
}
Note, this singleton will be consumed by both Angular and React apps.
Related
I want to write a plugin that contain all needed services and use this services easily when is needed.
I have wrote a plugin but it doesn't work !!
How to know what is wrong ?
This is plugin i wrote .
export default ({ $axios}:any , inject:any) => {
class BlogService {
getPost() {
return $axios.$get('/posts')
}
delete(id: Number) {
return $axios.Sdelete('/post/' + id)
}
}
inject(new BlogService)
}
And this is error i get
The inject method takes in two parameters. The first parameter is a string and will be the name of the property and then the second argument is the value (in your case, new BlogService()). I would also add the value to the context, so that you can access your service in asyncData and anywhere else the context is available.
Here's a rewritten version of your snippet, with these changes applied:
export default (context, inject) => {
const { $axios } = context
class BlogService {
getPost() {
return $axios.$get('/posts')
}
delete(id) {
return $axios.Sdelete('/post/' + id)
}
}
const blogService = new BlogService()
context.$blogService = blogService // Adds $blogService to the context.
inject('blogService', blogService) // Adds $blogService to all of your components.
}
While you haven't tagged your post with TypeScript, it looks as if you're using TypeScript in your original snippet, so I've also rewritten the snippet for TypeScript as you'll need to extend the default types with your new injected property. This is just to stop your linter from going crazy and your code completion to work.
import { Context, Plugin } from '#nuxt/types'
class BlogService {
readonly context: Context
constructor (context: Context) {
this.context = context
}
getPost() {
return this.context.$axios.$get('/posts')
}
delete(id: number) {
return this.context.$axios.Sdelete('/post/' + id)
}
}
// Create a new interface so that you don't have to add the new properties to all of the existing types every time.
interface Services {
$blogService: BlogService
}
declare module 'vue/types/vue' {
interface Vue extends Services {
}
}
declare module '#nuxt/types' {
interface NuxtAppOptions extends Services {
}
interface Context extends Services {
}
}
const servicesPlugin: Plugin = (context, inject) => {
const { $axios } = context
const blogService = new BlogService(context)
context.$blogService = blogService // Adds $blogService to the context.
inject('blogService', blogService) // Adds $blogService to all of your components.
}
export default servicesPlugin
i used above code and there is some error here .#tarkan
I started by following the answer in this StackOverflow Question
But I added a helper function I use which creates a new Axios instance with the auth token associated with the user.
Which looks a little like this:
import axios from "axios";
const mockAxios: jest.Mocked<typeof axios> = jest.createMockFromModule("axios");
// this is the key to fix the axios.create() undefined error!
mockAxios.create = jest.fn(() => {
return mockAxios;
});
export const createAuthenticatedInstance = () => {
return mockAxios.create();
};
export default mockAxios;
Why does mockAxios.create() return undefined?
While the object 'mockAxios' (and the create function) is defined. When I actually call create it returns undefined despite the function being declared.
I know that I can side-step the issue by just returning mockAxios against but I'd like to understand why it doesn't work in the first place. What I'd expect is to return a new instance, which would be identical to mockAxios but it just returns undefined.
If you're creating an auto-mock (within __mocks__) it's meant to be a mock of the module and any helper functions are not expected to be within the module, but probably somewhere else with your code
Exmaple:
src/axios.utils.ts (utility module which exports axios and the function)
import axios from "axios";
export const createAuthenticatedInstance = (
...args: Parameters<typeof axios.create>
) => {
return axios.create(...args);
};
export default axios;
src/__mocks__/axios.ts (the axios mock)
const axios: jest.Mocked<typeof import("axios").default> = jest.createMockFromModule(
"axios"
);
axios.create.mockReturnThis();
export default axios;
src/api.ts (api implementation that uses the axios.util)
import axios from "axios";
import { createAuthenticatedInstance } from "./axios.utils";
const client = createAuthenticatedInstance({
baseURL: "http://example.com:80/main",
});
export default {
makeSomeReq: () => client.get<string>("/path"),
};
src/api.spec.ts (the test)
import api from "./api";
import axios, { AxiosResponse } from "axios";
jest.mock("axios");
const { get } = axios as jest.Mocked<typeof import("axios").default>;
describe("api", () => {
it("should have created an axios instance", () => {
expect(axios.create).toHaveBeenCalledWith({
baseURL: "http://example.com:80/main",
});
});
})
working example
I'm trying to call an action in my vue from my store.
This is my file aliments.js in my store:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex, axios);
export const state = () => ({
aliments: {},
})
export const mutations = () => ({
SET_ALIMENTS(state, aliments) {
state.aliments = aliments
}
})
export const actions = () => ({
async getListAliments(commit) {
await Vue.axios.get(`http://localhost:3080/aliments`).then((response) => {
console.log(response);
commit('SET_ALIMENTS', response);
}).catch(error => {
throw new Error(`${error}`);
})
// const data = await this.$axios.get(`http://localhost:3080/aliments`)
// commit('setUser', user)
// state.user = data;
// return state.user;
}
})
export const getters = () => ({
aliments (state) {
return state.aliments
}
})
I want to diplay a list of aliments in my vue with :
{{ this.$store.state.aliments }}
I call my action like this :
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters(['loggedInUser', 'aliments']),
...mapActions(['getListAliments']),
getListAliments() {
return this.$state.aliments
}
}
}
</script>
I don't understand where is my mistake :/
NB: I also tried with a onclick method on a button with a dispatch('aliments/getListAliments')... but doesn't work...
The problem is that you're mapping your actions in the "computed" section of the component, you should map it in the "methods" section !
Hi and Welcome to StackOverflow
to quickly answer to your question, you would call an action as:
this.$store.dispatch('<NAME_OF_ACTION>', payload)
or though a mapActions as
...mapActions(['getListAliments']), // and you call `this.getListAliments(payload)`
or yet
...mapActions({
the_name_you_prefer: 'getListAliments' // and you call `this.the_name_you_prefer(payload)`
}),
for getters, it's the same process, as you already have 2 definitions ['loggedInUser', 'aliments'] you simply call the getter like if it was a computed value <pre>{{ aliments }}</pre>
or when we need to do a bit more (like filtering)
getListAliments() {
return this.$store.getters['aliments']
}
But I can see your store is as we call, one-to-rule-them-all, and because you are using Nuxt, you can actually leverage the module store very easy
as your application grows, you will start store everything in just one store file (the ~/store/index.js file), but you can easily have different stores and instead of what you wrote in index.js it can be easier if you had a file called, taken your example
~/store/food.js with
import axios from 'axios'
export const state = () => ({
aliments: {},
})
export const getters = {
aliments (state) {
return state.aliments
}
}
export const mutations = {
SET_ALIMENTS(state, aliments) {
state.aliments = aliments
}
}
export const actions = {
async getListAliments(commit) {
await axios.get('http://localhost:3080/aliments')
.then((response) => {
console.log(response);
commit('SET_ALIMENTS', response.data);
}).catch(error => {
throw new Error(`${error}`);
})
}
}
BTW, remember that, if you're using Nuxt serverMiddleware, this line
axios.get('http://localhost:3080/aliments')...
would simply be
axios.get('/aliments')...
and to call this store, all you need is to prefix with the filename, like:
...mapActions(['food/getListAliments'])
// or
...mapActions({ getListAliments: 'food/getListAliments' })
// or
this.$store.commit('food/getListAliments', payload)
another naming that could help you along the way:
on your action getListAliments you're actually fetching data from the server, I would change the name to fetchAliments
on your getter aliments you're actually returning the list, I would name it getAllAliments
have fun, Nuxt is amazing and you have a great community on Discord as well for the small things :o)
EDIT
also remember that actions are set in methods
so you can do:
...
export default {
methods: {
...mapActions(['getListAliments]),
},
created() {
this.getListAliments()
}
}
and in your Store action, please make sure you write
async getListAliments({ commit }) { ... }
with curly braces as that's a deconstruction of the property passed
async getListAliments(context) {
...
context.commit(...)
}
I have a subscriber that dispatches an action based on the parameters supplied to a pub event
// subscriptions.js
import store from '../store';
import { action } from '../actions';
export const subscribeToToggle = () => {
window.$.subscribe('action/toggle', (_e, isToggleOn) => {
if (isToggleOn=== true){
store.dispatch(action());
}
});
};
In my test file I write 2 tests that test that the action is sent only when true is supplied.
// subscriptions.test.js
import { subscribeToToggle } from './subscriptions';
import jQuery from 'jquery';
import { actionTypes } from '../constants';
import store from '../store';
jest.mock('../store');
beforeEach(() => {
window.$ = jQuery;
window.$.unsubscribe('action/toggle');
});
test('Action not sent when false', () => {
subscribeToToggleOpen();
window.$.publish('action/toggle', false);
expect(store.getActions().length).toBe(0);
});
test('Action sent when true', () => {
subscribeToToggleOpen();
window.$.publish('action/toggle', true);
expect(store.getActions().length).toBe(1);
expect(store.getActions()[0].type).toBe(actionTypes.ACTION);
});
I have the following mocked store using redux-test-utils
import { createMockStore } from 'redux-test-utils';
let store = null;
store = createMockStore('');
export default store;
The issue I face is that my test only pass when the false test comes first. If they are the other way around the 'Action not sent when false' test fails as it sees the action supplied by the 'Action sent when true' test.
Is there any way for me to use the beforeEach method to reset the mocked store object?
In this case, the problem is that your store is essentially a singleton. This can create issues when you are trying to do things like this and is generally kind of an anti-pattern.
Instead of exporting a store object, it'd probably be better if you exported a getStore() function which could be called to get the store. In that case, you could then do:
getStore().dispatch(action());
Inside of that, you could then have other helper functions to be able to replace the store that is being returned by it. That file could look something like this:
import { createMockStore } from 'redux-test-utils';
let store = createMockStore('');
export default () => store;
Then, inside of there, you can add another which could be resetStore as a non-default export:
export const resetStore = () => store = createMockStore('');
It would still technically be a singleton, but it's now a singleton you can have some control over.
Then, in your beforeEach() in your tests, just call resetStore():
import { resetStore } from '../store';
beforeEach(() => {
resetStore();
});
This would also require you to update your real code to use getStore() instead of store directly, but it'll probably be a beneficial change in the long run.
Complete Updated Version:
// subscriptions.js
import getStore from '../store';
import { action } from '../actions';
export const subscribeToToggle = () => {
window.$.subscribe('action/toggle', (_e, isToggleOn) => {
if (isToggleOn=== true){
getStore().dispatch(action());
}
});
};
// subscriptions.test.js
import { subscribeToToggle } from './subscriptions';
import jQuery from 'jquery';
import { actionTypes } from '../constants';
import getStore, { resetStore } from '../store';
jest.mock('../store');
beforeEach(() => {
window.$ = jQuery;
window.$.unsubscribe('action/toggle');
resetStore();
});
test('Action not sent when false', () => {
subscribeToToggleOpen();
window.$.publish('action/toggle', false);
expect(getStore().getActions().length).toBe(0);
});
test('Action sent when true', () => {
subscribeToToggleOpen();
window.$.publish('action/toggle', true);
expect(getStore().getActions().length).toBe(1);
expect(getStore().getActions()[0].type).toBe(actionTypes.ACTION);
});
import { createMockStore } from 'redux-test-utils';
let store;
export const resetStore = () => { store = createMockStore(''); }
resetStore(); // init the store, call function to keep DRY
export default () => store;
Beyond that, the other way would be to have a global reducer which could reset the state of the store to it's default, but that would be messier and I don't really think would generally fit with writing a unit test.
I am trying to spy on RxJS operators with Jasmine. There are different use cases in my tests where I want to be in control on what a Observable returns. To illustrate what I am trying to do I have created the example above even thought it does not make to much sense as this observable always returns the same hard coded string. Anyway it is a good example to show what I am trying to achieve:
Imagine I have the following Class.
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
export class AwesomeTest {
constructor() {
}
getHero(): Observable<string> {
return of('Spiderman');
}
}
And the following test:
import {AwesomeTest} from './awesomTest';
import {of} from 'rxjs/observable/of';
import createSpyObj = jasmine.createSpyObj;
import createSpy = jasmine.createSpy;
describe('Awesome Test', () => {
let sut;
beforeEach(() => {
sut = new AwesomeTest()
})
fit('must be true', () => {
// given
const expectedHero = 'Superman'
const asserter = {
next: hero => expect(hero).toBe(expectedHero),
error: () => fail()
}
createSpy(of).and.returnValue(of('Superman'))
// when
const hero$ = sut.getHero()
// then
hero$.subscribe(asserter)
});
});
I try to spy on the Observable of operator and return a Observable with a value that I specified inside my test instead of the actual value it will return. How can I achieve this?
Before the new Rx Import Syntax I was able to do something like this:
spyOn(Observable.prototype,'switchMap').and.returnValue(Observable.of(message))
In your spec file, everything as a wildcard (don't worry about tree shaking, this is just for the tests)
import * as rxjs from 'rxjs';
You can then use rxjs for your spying
spyOn(rxjs, 'switchMap').and.returnValue(rxjs.of(message))