I'm new on redux-saga/observable stuff. But I couldn't handle my scenario which looks so fit on these. So; I want to call API if any changes happen on the form. But I don't want to call API a lot because of the performance reasons.
Basically, when I trigger SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED, I want to call also getData function. But I need to put a delay to check other actions. For example; If SLIDERCHANGED is triggered, It should wait for 1sn and if TEXTBOXCHANGED is triggered at that time, It will be canceled and wait for 1sn more to call the getData function. So that's why I tried to implement Redux-saga or redux-observable.
I have actions types;
const SLIDERCHANGED = 'APP/sliderchanged';
const TEXTBOXCHANGED = 'APP/textboxchanged';
const CHECKBOXCHECKED = 'APP/checkboxchecked';
const LOADING = 'APP/loading';
const LOADDATA = 'APP/loaddata';
const ERRORLOADDATA = 'APP/errorloaddata';
I have also actions;
export function updateSliderValue(val) {
return { type: SLIDERCHANGED, val };
}
export function updateTextboxValue(val) {
return { type: TEXTBOXCHANGED, val };
}
export function updateCheckboxValue(val) {
return { type: CHECKBOXCHECKED, val };
}
export function loading() {
return { type: LOADING };
}
export function loadData(data) {
return { type: LOADDATA, data };
}
export function errorLoadData(err) {
return { type: ERRORLOADDATA, err };
}
export function getData(vehicleInfo) { // redux-thunk ASYNC function
return (dispatch, getState) => {
dispatch(loading());
return dispatch(apiCall('/getAllData', {}))
.then(payload => {
dispatch(loaddata(payload));
})
.catch(error => {
dispatch(errorLoadData(error));
});
};
}
With redux-saga, I did this but it doesn't work. It calls a getData function on each change with 1sn delay.
import { put, throttle } from 'redux-saga/effects';
import {
SLIDERCHANGED,
TEXTBOXCHANGED,
CHECKBOXCHECKED
} from './constants';
import { getData } from './actions';
function* onFormUpdate() {
yield put(getData());
}
export function* watchFormChange() {
yield throttle(
10000,
[SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED],
onFormUpdate
);
}
With redux-observable, Also somehow I get the same error.
import { ofType } from 'redux-observable';
import { delay, map, debounceTime } from 'rxjs/operators';
import {
SLIDERCHANGED,
TEXTBOXCHANGED,
CHECKBOXCHECKED
} from './constants';
import { getData } from './actions';
export const onFormUpdate = action => {
return action.pipe(
ofType(SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED),
debounceTime(1000),
map(() => getData())
);
};
Does anyone have any idea or opinion to make this happen?
Without having tested it, I think you can write a solution like this:
import { delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
function* onFormUpdate() {
yield delay(1000)
yield put(getData())
}
export function* watchFormChange() {
yield takeLatest(
[SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED],
onFormUpdate,
)
}
The idea here is that takeLatest will cancel onFormUpdate as soon as another one of the three actions is dispatched. So while onFormUpdate is in delay and then canceled, the next step, which is put, will not be called anymore.
Related
I want to test if "onLogin" event emitted from child component will trigger "toLogin" function from parent correctly.
Login.vue
<template>
<ChildComponent
ref="child"
#onLogin="toLogin"
/>
</template>
<script>
import { useAuthStore } from "#/stores/AuthStore.js"; //import Pinia Store
import { userLogin } from "#/service/authService.js"; // import axios functions from another js file
import ChildComponent from "#/components/ChildComponent.vue";
export default {
name: "Login",
components: {
ChildComponent,
},
setup() {
const AuthStore = useAuthStore();
const toLogin = async (param) => {
try {
const res = await userLogin (param);
AuthStore.setTokens(res);
} catch (error) {
console.log(error);
}
};
}
</script>
login.spec.js
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { shallowMount, flushPromises } from '#vue/test-utils';
import { createTestingPinia } from "#pinia/testing";
import Login from "#/views/user/Login.vue"
import { useAuthStore } from "#/stores/AuthStore.js";
describe('Login', () => {
let wrapper = null;
beforeAll(() => {
wrapper = shallowMount(Login, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })],
},
});
})
it('login by emitted events', async () => {
const AuthStore = useAuthStore();
const loginParam = {
email: 'dummy#email.com',
password: '12345',
};
const spyOnLogin = vi.spyOn(wrapper.vm, 'toLogin');
const spyOnStore = vi.spyOn(AuthStore, 'setTokens');
await wrapper.vm.$refs.child.$emit('onLogin', loginParam);
await wrapper.vm.$nextTick();
await flushPromises();
expect(spyOnLogin).toHaveBeenCalledOnce(); // will not be called
expect(spyOnStore).toHaveBeenCalledOnce(); // will be called once
})
}
I expected both "spyOnLogin" and "spyOnStore" will be called once from emitted event, however, only "spyOnStore" will be called even though "spyOnStore" should only be called after "spyOnLogin" has been triggered.
The error message is:
AssertionError: expected "toLogin" to be called once
❯ src/components/__tests__:136:24
- Expected "1"
+ Received "0"
What do I fail to understand about Vitest & Vue-Test-Utils?
You shouldn't mock your toLogin method because its part of Login component which you are testing. Therefore, instead of expecting if toLogin has been called, you should check if instructions inside are working correctly.
In your case i would only test if after emit, userLogin and AuthStore.setTokens has been called.
I have found following DataTable code snippet by PrimeVue, that uses the new Compositon API
<script>
import { ref, onMounted } from 'vue';
import ProductService from './service/ProductService';
export default {
setup() {
onMounted(() => {
productService.value.getProductsSmall().then(data => products.value = data);
})
const products = ref();
const productService = ref(new ProductService());
^^^^^^^^^^^^^^^^^^^^^^^^^^
return { products, productService }
}
}
</script>
Does it make any sense to ref() the ProductService?
I guess it does not. Am I wrong?
I believe you are correct, the assignment to ref is unnecessary.
I thought that might have been added for consistence with the options API:
import ProductService from './service/ProductService';
export default {
data() {
return {
products: null
}
},
productService: null,
created() {
this.productService = new ProductService();
},
mounted() {
this.productService.getProductsSmall().then(data => this.products = data);
}
}
However the productService is not part of the data object, so it is not reactive, and because it is a service that doesn't hold state it doesn't need to be.
I need a custom hook that uses Redux's state. If you were to pass the state from a React component to the function it would look something like:
Custom hook:
function useMyCustomHook(state) {
const { message } = state;
const handleClick = () => {
if(environment_variable) {
// do something with message
} else {
// do something else with message
}
}
return handleClick;
}
My component:
const MyComponent = ({ state }) => {
return <button onClick={()=> useMyCustomHook(state) }>Go</button>
}
It's a bit of a pain to have to pass Redux's state from the React component every time. Is it possible to access the state directly in the custom hook?
With the latest versions of react-redux you could use useSelector hook.
Also note that a hook is not supposed to be called on an handler
import { useSelector } from 'react-redux';
function useMyCustomHook() {
const message = useSelector(state => state.message);
const handleClick = () => {
if(environment_variable) {
// do something with message
} else {
// do something else with message
}
}
return handleClick;
}
and it will be used like
const MyComponent = ({ state }) => {
const handleClick = useMyCustomHook();
return <button onClick={handleClick}>Go</button>
}
I created a rootSaga in sagas.js as
function* fetchStuff(action) {
try {
yield put({type: 'INCREMENT'})
yield call(delay, 1000)
yield put({type: 'DECREMENT'})
const highlights = yield call(API.getStuff, action.data.myObject);
} catch (e) {
yield put({type: 'FETCH_STUFF_FAILED', message: e});
}
}
export default function* rootSaga() {
yield takeEvery('INIT_LOAD', fetchStuff);
}
I am calling the INIT_LOAD after thirdParty.method:
class myClass extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.load();
}
load = () => {
this.init = () => {
this.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObject: this.myObject
}
};
store.dispatch(action);
});
};
this.init();
};
render() {
return (
<div id="render-here" />
);
}
Passing the this.myObject in the action that is dispatched does not trigger the saga. If I change the action payload to a string, like the following, the saga is triggered.
const action = {
type: 'INIT_LOAD',
payload: {
myObject: 'this.myObject'
}
};
Why am I unable to pass this.myObject but a string is ok?
UPDATE: It is not a saga issue. I replicated the same issue with just plain redux. The rootReducer as
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INIT_LOAD':
return Object.assign({}, state, { myObject: action.payload.myObject });
default:
return state;
}
}
As I mentioned in the comment below, assigning it to an object Obj does not change the issue
let Obj = {};
...
load = () => {
this.init = () => {
Obj.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObj: Obj
}
};
store.dispatch(action);
});
};
this.init();
};
UPDATE2
I cleaned the code up & simply dispatched an action in the component that triggers the saga. Inside the saga is where I do the init(). I ran into another issue where the object that I was trying to save in the redux store has active socket sessions (which were given me cross-domain issues). Although I didn't solve my original problem, not storing a socket object made my problem go away.
My component get some properties via props with the function:
const mapStateToProps = state => {
const { entities: { keywords } } = state
const {locale} = state
return {
keywords: keywords[locale]
}
}
I got state keywords using ajax, in the same component:
componentDidMount() {
this.props.loadKeywords()
}
My component gets rendered twice. First, before the ajax resolves, so in my render method I got undefined:
render() {
const { keywords } = this.props.keywords
...
Which is the proper way to solve it? I changed componentDidMount to componentWillMount without success.
Right now, based on the real-world example, I have initialized keywords state with an empty object:
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
My reducer:
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import merge from 'lodash/merge'
import locale from './modules/locale'
import errorMessage from './modules/error'
import searchText from './modules/searchText'
// Updates an entity cache in response to any action with response.entities.
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
export default combineReducers({
locale,
router,
searchText,
errorMessage,
entities
})
My action:
import { CALL_API, Schemas } from '../middleware/api'
import isEmpty from 'lodash/isEmpty'
export const KEYWORDS_REQUEST = 'KEYWORDS_REQUEST'
export const KEYWORDS_SUCCESS = 'KEYWORDS_SUCCESS'
export const KEYWORDS_FAILURE = 'KEYWORDS_FAILURE'
// Fetches all keywords for pictos
// Relies on the custom API middleware defined in ../middleware/api.js.
function fetchKeywords() {
return {
[CALL_API]: {
types: [ KEYWORDS_REQUEST, KEYWORDS_SUCCESS, KEYWORDS_FAILURE ],
endpoint: 'users/56deee9a85cd6a05c58af61a',
schema: Schemas.KEYWORDS
}
}
}
// Fetches all keywords for pictograms from our API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadKeywords() {
return (dispatch, getState) => {
const keywords = getState().entities.keywords
if (!isEmpty(keywords)) {
return null
}
return dispatch(fetchKeywords())
}
}
All based on the Real world redux example
My Solution
Given initial state to keywords entity. I'm getting json like this through ajax:
{'locale': 'en', 'keywords': ['keyword1', 'keyword2']}
However as I use normalizr with locale as id, for caching results, my initial state is as I describe in the reducer:
function entities(state = { users: {}, repos: {}, keywords: { 'en': { 'keywords': [] } } }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
What I don't like is the initial if we have several languages, also remembering to modify it if we add another language, for example fr. In this
keywords: { 'en': { 'keywords': [] } }
should be:
keywords: { 'en': { 'keywords': [] }, 'fr': { 'keywords': [] } }
This line looks problematic:
const { keywords } = this.props.keywords
It's the equivalent of:
var keywords = this.props.keywords.keywords;
I doubt that's what you intended.
Another thing worth checking is keywords[locale] in your mapStateToProps() which will probably initially resolve to undefined. Make sure your component can handle that, or give it a sensible default.