substate.get() is not a function using React Boilerplate - reselect

I have a component called Login, and these selectors:
const selectLogin = () => (state) => state.get('login');
const selectUser = () => createSelector(
selectLogin(),
(loginState) => loginState.get('user')
);
Here's what state looks like for the "login" component:
login: {
user: {
id: 206
}
}
In another component, I want to select the "user" object.
At the top of my file, I have
import { createStructuredSelector } from 'reselect';
import {
selectLogin,
selectUser
} from 'containers/Login/selectors';
const mapStateToProps = createStructuredSelector({
login: selectLogin(),
user: selectUser(),
});
When I use "selectUser()", I get "loginState.get is not a function".
If I remove all references to "selectUser()", I can access this.props.login.user. That works for me, but I want to know why I can't select from within the "login" state. The examples use the same "substate" convention in the selector, and they work. Any ideas?

Is this another component in another route?
You have to manually inject reducers and sagas required for the page in each route.
In route.js, loadReducer and inject it to the page, something like this:
{
path: '/projects/add',
...
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/Project/reducer'),
System.import('containers/Login/reducer')
...
]);
const renderRoute = loadModule(cb);
importModules.then(([projectReducer, loginReducer ...]) => {
injectReducer('projects', projectReducer.default);
injectReducer('login', projectReducer.default);
renderRoute(component);
});
importModules.catch(errorLoading);
},

Related

Dispatch actions from a custom hook using useQuery

I'm trying to write a custom hook that uses useQuery from react-query. The custom hook takes in the id of an employee and fetches some data and returns it to the consuming component. I want to be able to dispatch a redux action to show a loading indicator or show an error message if it fails. Here is my custom hook.
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
}
const query = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
})
if (query.isFetching || query.isLoading) {
dispatch(setWaiting())
}
return query.data
}
When I refresh the page, I get this error in the browser's console and I'm not sure how to fix this error?
Warning: Cannot update a component (`WaitIndicator`) while rendering a different component (`About`).
To locate the bad setState() call inside `About`, follow the stack trace as described in
The issue is likely with dispatching the setWaiting action outside any component lifecycle, i.e. useEffect. Move the dispatch logic into a useEffect hook with appropriate dependency.
Example:
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
};
const { data, isFetching, isLoading } = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
});
useEffect(() => {
if (isFetching || isLoading) {
dispatch(setWaiting());
}
}, [isFetching, isLoading]);
return data;
}

Cypress using actions from Pinia Vue3

I was learning some cypress from this video: https://www.youtube.com/watch?v=03kG2rdJYtc
I'm interested with he's saying at 29:33: "programatic login"
But he's using vue2 and Vuex.
My project is created with Vite and the state management is Pinia.
So how can I do a programatic login using the pinia action?
For example the welcome logged in user should see dashboard:
describe('Welcome', () => {
it('logged in user should visit dashboard', () => {
// login
cy.visit('/')
cy.url().should('contain', '/dashboard')
})
})
And my userStore:
export const useUserStore = defineStore({
id: 'user',
state: () => ({
username: ref(useLocalStorage('username', null)),
}),
getters: {
isLoggedIn: (state) => state.username !== null,
},
actions: {
login(username, password) {
return useAuthLoginService(username, password)
.then((response) => {
this.username = response.username
})
.catch((error) => {
return Promise.reject(new Error(error))
})
},
},
})
How can I call the login action on the cypress test?
For now as a workaround I'm writing on a localstorage like:
localStorage.setItem('username', 'user')
And it works fine, because userStore catch this item from localstorage and passes like it's logged in... But I don't like this solution, seems fragile, and I'd like to use the action which is made for login users.
Another thing I tried is adding the app variable inside window but it doesn't work for me... don't understand why...
on main.js
The video shows that code:
const vue = new Vue({...})
if(window.Cypress){
window.app = app
}
In my case it's:
const app = createApp(App)
if(window.Cypress){
window.app = app
}
But in cypress tests the window.app it's undefined... I don't know how I would access to userStore using this... like it was vuex.
Using the Pinia demo app as an example:
The store is initialized in App.vue. Add a reference to the newly created store(s) for Cypress to use
export default defineComponent({
components: { Layout, PiniaLogo },
setup() {
const user = useUserStore()
const cart = useCartStore()
if (window.Cypress) {
window.store = {user, cart) // test can see window.store
}
...
In the test
let store;
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`)
.then(win => store = win.store) // get app's store object
})
it('works', () => {
cy.wait(500) // wait for the JS to load
.then(() => store.cart.addItem('Cypress test item')) // invoke action
.then(() => {
const item1 = store.cart.items[0] // invoke getter
cy.wrap(item1)
.should('have.property', 'name', 'Cypress test item') // passes
})
The login action is asynchronous, so return the promise to allow Cypress to wait.
// user.js
async login(user, password) {
const userData = await apiLogin(user, password)
this.$patch({
name: user,
...userData,
})
return userData // this returns a promise which can awaited
},
// main.spec.js
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store before login
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging in
store.user.login('ed', 'ed').then(() => { // wait for API call
cy.wrap(store.user.name).should('eq', 'ed')
})
})
})
Alternatively, wait for the name to change on the page
// main.spec.js
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging on
store.user.login('ed', 'ed')
cy.contains('Hello ed') // waits for name on page to change
.then(() => {
cy.wrap(store.user.name).should('eq', 'ed')
})
})

Vue + axios returns undefined

I have app.js importing axios and VueAxios as:
Vue.use(VueAxios, axios);
Then calling my component:
Vue.component('api-call', require('./components/PostComponent'));
In my PostComponent I have a simple axios get as follows:
<script>
export default {
// name: "PostComponent"
data() {
return {
post: {},
}
},
methods: {
getPosts: () => {
console.log('started');
//let that = this;
let uri = 'https://jsonplaceholder.typicode.com/posts';
this.axios.get(uri).then((response) => {
console.log(response);
})
}
},
mounted(){
this.getPosts()
}
}
</script>
Since I want this executed right at the start of the component loading I am using mounted (why Vue don't have a constructor baffles me, even react passed on the isMounted pattern.)
What am I doing wrong?
thanks,
Bud
You can't use arrow function for methods declaration.
See https://v2.vuejs.org/v2/api/#methods
Note that you should not use an arrow function to define a method
(e.g. plus: () => this.a++). The reason is arrow functions bind the
parent context, so this will not be the Vue instance as you expect and
this.a will be undefined.
These are the 2 ways to properly define a method
1.
getPosts: function() {
}
(if you can use ES6)
getPosts() {
}

Vuejs Unit Test - Backing Mocks with Tests

I am writing unit testing for a vuejs 2 application that uses Vuex as a store. I have the following pattern in many of my components:
example component thing.vue:
<template>
<div>
{{ thing.label }}
</div>
</template>
<script>
export default {
name: 'thing',
data() { return { } },
computed: {
thing () {
return this.$store.state.thing;
}
}
}
</script>
Example Store State:
export const state = {
thing: { label: 'test' }
};
Example Unit for Thing.vue:
describe('thing ', () => {
const storeMock = new Vuex.Store( state: { thing: { label: 'test' } } );
it('should pull thing from store', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store }).$mount();
expect(component.thing).toEqual({ label: 'test' });
});
});
Example Unit test for Store:
import store from './store';
describe('Vuex store ', () => {
it('should have a thing object', () => {
expect(store.state.thing).toEqual({ label: 'test' });
});
});
There is a huge problem with this pattern. When another developer refractors the store state, they will see the Store test fail, but because the thing unit test is based on a mocked version of the store that test with continue to pass, even though that component will never work. There isn't a good way to know a refactor invalidated a Mock.
So how do people unit test this type of dependence?
One way would be to cheat a little on the unit test and use the real store state, but then it isn't really a unit test. The other way is rely on integration testing to catch the mock - store mismatch, but that feels like it would be painful to debug why the unit tests pass but the integration tests are failing.
What we ended up doing is using the actual store. Because the store state is just an object we figured it was acceptable.
We also use the store getters, actions and mutations as templates for jasmine spyies.
// Vuex needs polyfill
import { polyfill } from 'es6-promise';
polyfill();
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import test from 'app/components/test.vue';
import module from 'app/store/modules/module';
describe('Spec for Test.vue', () => {
var props;
var state;
var actions;
var mutations;
var getters;
var store;
beforeEach( () => {
jasmine.addMatchers(customMatchers);
props = { };
// Don't change the modules
state = Object.assign({}, module.state);
actions = Object.assign({}, module.actions);
mutations = Object.assign({}, module.mutations);
getters = Object.assign({}, module.getters);
// Add require global actions, mutations, and getters here...
actions.globalActionHere = 'anything'; // this turns into a spy
// Update State with required fields
state.defaults = { id: 1 } // default expected when the component loads
// Replace modules copies with mocks
actions = jasmine.createSpyObj('actions', actions);
mutations = jasmine.createSpyObj('mutations', mutations);
getters = jasmine.createSpyObj('getters', getters);
store = new Vuex.Store( { state: { module: state }, getters, actions, mutations } );
} );
it('should have a name of test', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store, props }).$mount();
expect(component.$options.name).toBe('test');
});
});
Note the part
jasmine.createSpyObj('actions', actions);
Jasmine spies will use the module to create spyies for each of the methods, which is very useful.

How do you dynamically control react apollo-client query initiation?

A react component wrapped with an apollo-client query will automatically initiate a call to the server for data.
I would like to fire off a request for data only on a specific user input.
You can pass the skip option in the query options - but this means the refetch() function is not provided as a prop to the component; and it appears that the value of skip is not assessed dynamically on prop update.
My use is case is a map component. I only want data for markers to be loaded when the user presses a button, but not on initial component mount or location change.
A code sample below:
// GraphQL wrapping
Explore = graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
skip: ({ targetResource, searchIsAllowedForMapArea }) => {
const skip = Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE');
return skip;
},
}),
props: ({ ownProps, data: { loading, viewer, refetch }}) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})(Explore);
To include an HoC based on a condition affected by a props change, you could use branch from recompose.
branch(
test: (props: Object) => boolean,
left: HigherOrderComponent,
right: ?HigherOrderComponent
): HigherOrderComponent
check: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch
For this specific example, would look something like:
const enhance = compose(
branch(
// evaluate condition
({ targetResource, searchIsAllowedForMapArea }) =>
Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE'),
// HoC if condition is true
graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
}),
props: ({ ownProps, data: { loading, viewer, refetch } }) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})
)
);
Explore = enhance(Explore);
I have a similar use case, I wanted to load the data only when the user clicked.
I've not tried the withQuery suggestion given by pencilcheck above. But I've seen the same suggestion elsewhere. I will try it, but in the meantime this is how I got it working based off a discussion on github:
./loadQuery.js
Note: I'm using the skip directive:
const LOAD = `
query Load($ids:[String], $skip: Boolean = false) {
things(ids: $ids) #skip(if: $skip) {
title
}
`
LoadMoreButtonWithQuery.js
Here I use the withState higher-order function to add in a flag and a flag setter to control skip:
import { graphql, compose } from 'react-apollo';
import { withState } from 'recompose';
import LoadMoreButton from './LoadMoreButton';
import LOAD from './loadQuery';
export default compose(
withState('isSkipRequest', 'setSkipRequest', true),
graphql(
gql(LOAD),
{
name: 'handleLoad',
options: ({ids, isSkipRequest}) => ({
variables: {
ids,
skip: isSkipRequest
},
})
}
),
)(Button);
./LoadMoreButton.js
Here I have to manually "flip" the flag added using withState:
export default props => (
<Button onClick={
() => {
props.setSkipRequest(false); // setter added by withState
props.handleLoad.refetch();
}
}>+</Button>
);
Frankly I'm a little unhappy with this, as it is introduces a new set of wiring (composed in by "withState"). Its also not battle tested - I just got it working and I came to StackOverflow to check for better solutions.

Resources